import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
from datetime import date, datetime, timedelta
import matplotlib.pyplot as plt
import seaborn as sns
from statistics import mode
import scipy.stats as stats
## CONSTANTES ##
FILE_BINANCE = 'BTCUSDT_binance.csv'
FILE_BINANCEAPI = 'BTCUSDT_binanceapi.csv'
FILE_COINMARKETCAP = 'BTCUSD_coinmarketcap.csv'
FILE_BTCDIRECT = 'BTCUSD_btcdirect.csv'
FILE_YAHOOFINANCE = 'BTCUSD_yahoofinance.csv'
FILE_COINGECKO = 'BTCUSD_coingecko.txt'
FILE_FEDERALFUNDS = 'FRB_H15.csv'
FILE_SP500 = 'SP500.csv'
FILE_NASDAQ = 'NASDAQ.csv'
FILE_EUR_USD = 'EURUSD.csv'
FILE_GBP_USD = 'GBPUSD.csv'
FILE_JPY_USD = 'JPYUSD.csv'
## FUNCIONES ##
# Devuelve el mínimo y máximo de una variable dada
def var_min_max(_df, _var):
return (_df[_var].min(), _df[_var].max())
# Devuelve un dataframe con información de análisis univariante de variables cuantitativas continuas:
def univar_num_cont(_df, _list_var):
univar_data = []
for _var in _list_var:
univar_data.append([_var, _df[_var].nunique(), round(_df[_var].mean(),2), round(_df[_var].median(),2),
round(_df[_var].min(),2), round(_df[_var].max(),2), round(np.sqrt(np.var(_df[_var])),2)])
return pd.DataFrame(univar_data, columns=["Variable", "Valores Únicos", "Media", "Mediana", "Mínimo",
"Máximo", "Desv. Estándar"])
# Devuelve un dataframe con información de análisis univariante de variables cuantitativas discretas:
def univar_num_disc(_df, _list_var):
univar_data = []
for _var in _list_var:
univar_data.append([_var, _df[_var].min(), round(np.percentile(_df[_var], (25)),2), _df[_var].median(),
round(np.percentile(_df[_var], (75)),2), _df[_var].max(), mode(_df[_var]),
round(np.sqrt(np.var(_df[_var])),2)])
return pd.DataFrame(univar_data, columns=["Variable", "Mínimo", "Perc25", "Mediana", "Perc75", "Máximo",
"Moda", "Desv. Estándar"])
# Rellena valores nulos de una variable con su moda
def impute_mode(_df, _var):
return _df[_var].fillna(mode(_df[_var]))
# Rellena valores nulos de una variable con el valor no nulo inmediatamente anterior (u otra forma si se modifica _method)
def impute_forward_fill(_df, _var, _method='ffill'):
return _df[_var].fillna(method=_method)
# Rellena los valores nulos de una variable mediante interpolación lineal (u otros si se modifica _method)
def impute_interpolate(_df, _var, _method='linear', _limit_direction='forward', _axis=0):
return _df[_var].interpolate(method=_method, limit_direction=_limit_direction, axis=_axis)
# Para realizar una normalización z-score (incluye opción para realizarla en ventanas de tiempo)
def normalize_zscore(_df, _var, _window=0):
if(_window>0):
return _df[_var].rolling(window=_window, min_periods=1).apply(
lambda x: (x.iloc[-1] - x.mean()) / x.std(ddof=0) if x.std(ddof=0) > 0 else 0, raw=False)
else:
return (_df[_var] - _df[_var].mean()) / _df[_var].std(ddof=0)
# Devuelve un valor desnormalizado; emplear cuando el modelo haya dado una predicción de precio para conocer el valor en USDT
def denormalize_data(_data, _df, _var='close', _window=20, _index=None):
if _index is None:
_index = len(_df) - 1
serie = _df[_var]
# Calcular la media y desviación estándar de la ventana en el índice de predicción
if _index >= _window - 1: # Si el índice tiene suficientes datos para la ventana completa
ventana = serie.iloc[_index - _window + 1 : _index + 1]
else: # Si el índice está en el rango inicial y usa una ventana reducida
ventana = serie.iloc[: _index + 1]
mean_window = ventana.mean()
std_window = ventana.std(ddof=0)
# Desnormalizar el valor
if std_window > 0: # Evitar división por cero
return round((_data * std_window) + mean_window, 2)
else:
return round(mean_window, 2) # Si la desviación es 0, el valor normalizado corresponde a la media
# Para estandarizar el dataset original con el que se entrena y valida el modelo y posteriores datasets
def standard_dataset(_x, _var_list):
scaler = StandardScaler()
for var in _var_list:
_x[var] = scaler.fit_transform(_x[[var]])
return _x
# Cálculo del R2 ajustado
def adjusted_r2(_r2, _n, _p):
return 1 - ((1 - _r2) * (_n - 1) / (_n - _p - 1))
## INDICADORES TÉCNICOS ##
# SMA
def sma(_df, _window=7):
return _df['close'].rolling(window=_window).mean()
# RSI
def rsi(_df, _window=14):
delta = _df['close'].diff()
gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0)
avg_gain = gain.rolling(window=_window).mean()
avg_loss = loss.rolling(window=_window).mean()
rsi = avg_gain / avg_loss
return (100 - (100 / (1 + rsi)))
# Bandas de Bollinger
def bollinger_bands(_df, _sma=20):
return _df['sma_20'] + 2 * _df['close'].rolling(window=_sma).std(), \
_df['sma_20'] - 2 * _df['close'].rolling(window=_sma).std()
# MACD
def macd(_df, _ema1=12, _ema2=26, _signal_length=9):
ema_12 = _df['close'].ewm(span=_ema1, adjust=False).mean()
ema_26 = _df['close'].ewm(span=_ema2, adjust=False).mean()
macd = ema_12 - ema_26
return macd, macd.ewm(span=_signal_length, adjust=False).mean()
# Volatilidad histórica
def historical_volatility(_df, _window=20):
log_returns = np.log(_df['close'] / _df['close'].shift(1))
return log_returns.rolling(window=_window).std()
## GRÁFICOS ##
# Representa un histograma con líneas de mediana y media para una variable cuantitativa continua
def univar_graph_num_cont(_df, _var, _bins=False):
if _bins == False:
_bins = 20
media = _df[_var].mean()
mediana = _df[_var].median()
plt.figure(figsize=(10,6))
plt.axvline(mediana, color='orange', linestyle='-', label=f'Mediana: {mediana:.2f}')
plt.axvline(media, color='black', linestyle='-', label=f'Media: {media:.2f}')
sns.histplot(_df[_var], bins=_bins, kde=True)
plt.xlabel(_var)
plt.ylabel("Frecuencia")
plt.title(f"Histograma de '{_var}'")
plt.legend()
plt.show()
# Representa un gráfico de barras para una variable cuantitativa discreta
def univar_graph_num_disc(_df, _var):
values = _df[_var].value_counts().sort_index()
plt.figure(figsize=(10, 6))
values.plot(kind='bar', color='skyblue')
plt.title(f"Distribución de '{_var}'")
plt.ylabel("Frecuencia")
plt.xlabel(f"{_var}")
plt.xticks(rotation=65)
plt.show()
# Representa un gráfico boxplot de la distribución de una variable
def univar_graph_boxplot(_df, _var):
plt.figure(figsize=(12, 6))
sns.boxplot(x= df[_var])
plt.xticks(rotation=65)
plt.title(f'Boxplot de {_var}')
plt.show()
# Representa un scatterplot de comparación entre una variable cuantitativa y la variable objetivo
def bivar_graph_scatt(_df, _var, _obj_var='close'):
plt.figure(figsize=(10, 6))
sns.regplot(x=_var, y=_obj_var, data=_df, scatter_kws={"s": 20}, line_kws={"color": "red"})
plt.title(f"Comparación de {_var} y {_obj_var}")
plt.xlabel(_var)
plt.ylabel(_obj_var)
plt.show()
# Representa la evolución en el tiempo de una variable
def time_line(_df, _var_date='date', _vars=['close']):
plt.figure(figsize=(12, 6))
for _var in _vars:
sns.lineplot(data=_df, x=_var_date, y=_var, label=f"'{_var}'")
plt.title(f"Evolución de '{_vars}' en el tiempo", fontsize=16)
plt.xlabel('Tiempo', fontsize=12)
plt.ylabel(_vars, fontsize=12)
plt.xticks(rotation=45)
plt.legend()
plt.grid(True, linestyle='--', alpha=0.5)
plt.show()
# Representa la matriz de correlación de las variables numéricas del dataframe
def multivar_graph_correlation_matrix(_df, _method='pearson'):
corr = _df.corr(method=_method)
mask = np.triu(np.ones_like(corr, dtype=bool))
f, ax = plt.subplots(figsize=(11, 9))
cmap = sns.diverging_palette(230, 20, as_cmap=True)
sns.set_theme(style="white")
sns.heatmap(corr, mask=mask, cmap=cmap, vmax=1, vmin=-1, center=0,
square=True, linewidths=.5, cbar_kws={"shrink": .5});
# Representa un gráfico comparando cada predicción con su residuo
def residuals_vs_predictions(_pred, _resid):
plt.figure(figsize=(14, 6))
plt.scatter(_pred, _resid, color='orange', alpha=0.6)
plt.axhline(0, color='black', linestyle='--')
plt.title("Residuos vs Predicciones")
plt.xlabel("Predicciones")
plt.ylabel("Residuos")
# Representa un gráfico comparando los valores de test con los valores predichos por el modelo
def test_vs_estimacion(_y_test, _y_pred):
fig, ax = plt.subplots(figsize=(12,8))
plt.scatter(_y_test, _y_pred, color='royalblue')
plt.plot(_y_test, _y_test, color='black', linewidth=3)
plt.xlabel("Test")
plt.ylabel("Estimación")
plt.show()
# Representa un histograma de distribución de residuos
def residual_histogram(_residuals, _bins=20):
plt.figure(figsize=(14, 6))
plt.hist(_residuals, bins=_bins, color='orange', alpha=1)
plt.title("Histograma de Residuos")
plt.xlabel("Residuos")
plt.tight_layout()
plt.show()
# Representa un gráfico QQPlot para comparar la normalidad de los residuos
def residuals_qqplot(_residuals):
plt.figure(figsize=(14, 6))
stats.probplot(_residuals, dist="norm", plot=plt)
plt.title("QQPlot de Residuos")
plt.tight_layout()
plt.show()
## FICHEROS ##
# Guardar y recuperar datos csv.
def save_csv(_df, _coin1, _coin2, _date=False):
pair = _coin1 + _coin2
if(_date):
date = _df.date[0].strftime('%Y%m%d')
_df.to_csv(f'archivos/{pair}_{date}.csv', index=False)
else:
_df.to_csv(f'archivos/{pair}.csv', index=False)
def open_csv(_file):
return pd.read_csv(f'archivos/{_file}')
def open_json_file(_file):
with open(f'archivos/{_file}', 'r', encoding='utf-8') as file:
return json.load(file)
df_data_coinmarketcap = open_csv(FILE_COINMARKETCAP)
df_data_binanceapi = open_csv(FILE_BINANCEAPI)
df_data_federalfunds = open_csv(FILE_FEDERALFUNDS)
df_data_sp500 = open_csv(FILE_SP500)
df_data_nasdaq = open_csv(FILE_NASDAQ)
df_eur_usd = open_csv(FILE_EUR_USD)
df_gbp_usd = open_csv(FILE_GBP_USD)
df_jpy_usd = open_csv(FILE_JPY_USD)
El dataset inicial será el que contiene los datos de la API de Binance, es decir, df_data_binanceapi. A partir de él, se tratará de añadir nuevos datos del resto de conjuntos de datos.
df_inicial = df_data_binanceapi.copy()
df_inicial
| date | open | high | low | close | VOL_BTC | VOL_USDT | transactions | buy_market_BTC | buy_market_USDT | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-08-17 | 4261.48 | 4485.39 | 4200.74 | 4285.08 | 795.150377 | 3.454770e+06 | 3427 | 616.248541 | 2.678216e+06 |
| 1 | 2017-08-18 | 4285.08 | 4371.52 | 3938.77 | 4108.37 | 1199.888264 | 5.086958e+06 | 5233 | 972.868710 | 4.129123e+06 |
| 2 | 2017-08-19 | 4108.37 | 4184.69 | 3850.00 | 4139.98 | 381.309763 | 1.549484e+06 | 2153 | 274.336042 | 1.118002e+06 |
| 3 | 2017-08-20 | 4120.98 | 4211.08 | 4032.62 | 4086.29 | 467.083022 | 1.930364e+06 | 2321 | 376.795947 | 1.557401e+06 |
| 4 | 2017-08-21 | 4069.13 | 4119.62 | 3911.79 | 4016.00 | 691.743060 | 2.797232e+06 | 3972 | 557.356107 | 2.255663e+06 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2655 | 2024-11-23 | 98892.00 | 98908.85 | 97136.00 | 97672.40 | 24757.843670 | 2.431610e+09 | 3839138 | 11176.248700 | 1.097539e+09 |
| 2656 | 2024-11-24 | 97672.40 | 98564.00 | 95734.77 | 97900.04 | 31200.978380 | 3.034172e+09 | 4964720 | 15124.121760 | 1.471314e+09 |
| 2657 | 2024-11-25 | 97900.05 | 98871.80 | 92600.19 | 93010.01 | 50847.450960 | 4.883445e+09 | 8289691 | 23486.927050 | 2.255517e+09 |
| 2658 | 2024-11-26 | 93010.01 | 94973.37 | 90791.10 | 91965.16 | 57858.731380 | 5.370919e+09 | 10225809 | 29120.267650 | 2.702544e+09 |
| 2659 | 2024-11-27 | 91965.16 | 96539.33 | 91792.14 | 96316.00 | 32915.047770 | 3.100202e+09 | 5553374 | 16124.386890 | 1.518456e+09 |
2660 rows × 10 columns
Se descarta la observación para la fecha 27/11/2024 por no ser un día cerrado, por lo que, excepto el precio de apertura, el resto de campos son susceptibles de sufrir variaciones en su valor.
df_inicial = df_inicial[df_inicial['date'] != '2024-11-27']
df_inicial.tail()
| date | open | high | low | close | VOL_BTC | VOL_USDT | transactions | buy_market_BTC | buy_market_USDT | |
|---|---|---|---|---|---|---|---|---|---|---|
| 2654 | 2024-11-22 | 98317.12 | 99588.01 | 97122.11 | 98892.00 | 46189.309243 | 4.555537e+09 | 7271311 | 22709.668193 | 2.240635e+09 |
| 2655 | 2024-11-23 | 98892.00 | 98908.85 | 97136.00 | 97672.40 | 24757.843670 | 2.431610e+09 | 3839138 | 11176.248700 | 1.097539e+09 |
| 2656 | 2024-11-24 | 97672.40 | 98564.00 | 95734.77 | 97900.04 | 31200.978380 | 3.034172e+09 | 4964720 | 15124.121760 | 1.471314e+09 |
| 2657 | 2024-11-25 | 97900.05 | 98871.80 | 92600.19 | 93010.01 | 50847.450960 | 4.883445e+09 | 8289691 | 23486.927050 | 2.255517e+09 |
| 2658 | 2024-11-26 | 93010.01 | 94973.37 | 90791.10 | 91965.16 | 57858.731380 | 5.370919e+09 | 10225809 | 29120.267650 | 2.702544e+09 |
Los datasets obtenidos mediante scraping a la web de Binance, BtcDirect, Yahoo Finance y CoinGecko, serán descartados, el primero por ser igual pero con menor número de observaciones que el obtenido con la librería de Binance para Python, y los demás por aportar tan sólo valores de precio y volumen, datos que ya posee el conjunto que se ha escogido como inicial.
Por otro lado, del dataset obtenido haciendo scraping de CoinMarketCap, contiene dos variables extra que no posee el dataset inicial: la capitalización de mercado y el supply total de Bitcoin. Dado que la capitalización de mercado resulta de multiplicar el supply por el precio, incluir ambas variables en el dataset inicial supondría que, al realizar el análisis EDA, aparecería un problema de colinealidad perfecta que llevaría a tener que eliminar la variable en cuestión. Por ello, se toma la decisión de incluir en el dataset únicamente la variable total_supply y descartar market_cap.
Además, hay que tener en consideración que, para el dataset de CoinMarketCap tenemos únicamente una observación por semana y que comienza en 14/07/2010, por lo que habrá que eliminar todas las observaciones para fechas anteriores a 17/08/2017 e idear una estrategia para interpolar los datos para todas aquellas fechas que no lo poseen.
Dado que el supply total de Bitcoin es acumulativo y, por tanto sólo puede aumentar o permanecer constante en el tiempo (podría reducirse de forma excepcional por diversas razones, pero es poco probable), una estrategia aceptable podría ser rellenar los datos faltantes mediante una interpolación lineal.
df_coinmarketcap_modified = df_data_coinmarketcap.copy()
# Eliminación de las observaciones anteriores a 17/08/2017
df_coinmarketcap_modified = df_coinmarketcap_modified[df_coinmarketcap_modified['date'] >= "2017-08-17"]
# Se añaden las fechas para las que no existe observación
dates = pd.date_range(start='2017-08-17', end=df_inicial['date'].max())
df_dates = pd.DataFrame({'date': dates})
df_coinmarketcap_modified['date'] = pd.to_datetime(df_coinmarketcap_modified['date'])
df_coinmarketcap_modified = pd.merge(df_dates, df_coinmarketcap_modified, on='date', how='left')
# Interpolación de los datos faltantes de total_supply
df_coinmarketcap_modified['total_supply'] = df_coinmarketcap_modified['total_supply'].interpolate(method='linear')
# Extrapolación de los datos faltantes desde la primera fecha de la que se tiene valor para total_supply hasta el 17/28/2017
df_coinmarketcap_modified['total_supply'].fillna(method='bfill', inplace=True)
# Unión de la variable total_supply al dataset inicial
df_inicial['date'] = pd.to_datetime(df_inicial['date'])
df_inicial = pd.merge(df_inicial, df_coinmarketcap_modified[['date', 'total_supply']], on='date', how='left')
df_inicial
| date | open | high | low | close | VOL_BTC | VOL_USDT | transactions | buy_market_BTC | buy_market_USDT | total_supply | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-08-17 | 4261.48 | 4485.39 | 4200.74 | 4285.08 | 795.150377 | 3.454770e+06 | 3427 | 616.248541 | 2.678216e+06 | 1.651956e+07 |
| 1 | 2017-08-18 | 4285.08 | 4371.52 | 3938.77 | 4108.37 | 1199.888264 | 5.086958e+06 | 5233 | 972.868710 | 4.129123e+06 | 1.651956e+07 |
| 2 | 2017-08-19 | 4108.37 | 4184.69 | 3850.00 | 4139.98 | 381.309763 | 1.549484e+06 | 2153 | 274.336042 | 1.118002e+06 | 1.651956e+07 |
| 3 | 2017-08-20 | 4120.98 | 4211.08 | 4032.62 | 4086.29 | 467.083022 | 1.930364e+06 | 2321 | 376.795947 | 1.557401e+06 | 1.651956e+07 |
| 4 | 2017-08-21 | 4069.13 | 4119.62 | 3911.79 | 4016.00 | 691.743060 | 2.797232e+06 | 3972 | 557.356107 | 2.255663e+06 | 1.651956e+07 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2654 | 2024-11-22 | 98317.12 | 99588.01 | 97122.11 | 98892.00 | 46189.309243 | 4.555537e+09 | 7271311 | 22709.668193 | 2.240635e+09 | 1.978572e+07 |
| 2655 | 2024-11-23 | 98892.00 | 98908.85 | 97136.00 | 97672.40 | 24757.843670 | 2.431610e+09 | 3839138 | 11176.248700 | 1.097539e+09 | 1.978626e+07 |
| 2656 | 2024-11-24 | 97672.40 | 98564.00 | 95734.77 | 97900.04 | 31200.978380 | 3.034172e+09 | 4964720 | 15124.121760 | 1.471314e+09 | 1.978680e+07 |
| 2657 | 2024-11-25 | 97900.05 | 98871.80 | 92600.19 | 93010.01 | 50847.450960 | 4.883445e+09 | 8289691 | 23486.927050 | 2.255517e+09 | 1.978734e+07 |
| 2658 | 2024-11-26 | 93010.01 | 94973.37 | 90791.10 | 91965.16 | 57858.731380 | 5.370919e+09 | 10225809 | 29120.267650 | 2.702544e+09 | 1.978734e+07 |
2659 rows × 11 columns
Se obtiene finalmente un dataset inicial con las siguientes variables:
A continuación, se agregan datos adicionales:
df_inicial.columns
Index(['date', 'open', 'high', 'low', 'close', 'VOL_BTC', 'VOL_USDT',
'transactions', 'buy_market_BTC', 'buy_market_USDT', 'total_supply'],
dtype='object')
# Tasa de interés de los fondos federales de Estados Unidos
df_data_federalfunds.columns
Index(['Time Period', 'RIFSPFF_N.D'], dtype='object')
df_data_federalfunds.rename(columns={'Time Period': 'date', 'RIFSPFF_N.D': 'fed_funds_rate'}, inplace=True)
df_data_federalfunds = df_data_federalfunds[df_data_federalfunds['date'] >= "2017-08-17"]
df_data_federalfunds['date'] = pd.to_datetime(df_data_federalfunds['date'])
df_data_federalfunds
| date | fed_funds_rate | |
|---|---|---|
| 228 | 2017-08-17 | 1.16 |
| 229 | 2017-08-18 | 1.16 |
| 230 | 2017-08-19 | 1.16 |
| 231 | 2017-08-20 | 1.16 |
| 232 | 2017-08-21 | 1.16 |
| ... | ... | ... |
| 2882 | 2024-11-22 | 4.58 |
| 2883 | 2024-11-23 | 4.58 |
| 2884 | 2024-11-24 | 4.58 |
| 2885 | 2024-11-25 | 4.58 |
| 2886 | 2024-11-26 | 4.58 |
2659 rows × 2 columns
# Datos de precio de SP500
df_data_sp500.columns
Index(['Date', 'Open', 'High', 'Low', 'Close', 'Adj Close', 'Volume'], dtype='object')
df_data_sp500 = df_data_sp500.rename(columns={'Date': 'date', 'Close': 'sp500'})[['date', 'sp500']]
df_data_sp500 = df_data_sp500[df_data_sp500['date'] >= "2017-08-17"]
df_data_sp500['date'] = pd.to_datetime(df_data_sp500['date'])
df_data_sp500
| date | sp500 | |
|---|---|---|
| 1085 | 2017-08-17 | 2430.010010 |
| 1086 | 2017-08-18 | 2425.550049 |
| 1087 | 2017-08-21 | 2428.370117 |
| 1088 | 2017-08-22 | 2452.510010 |
| 1089 | 2017-08-23 | 2444.040039 |
| ... | ... | ... |
| 2912 | 2024-11-20 | 5917.109863 |
| 2913 | 2024-11-21 | 5948.709961 |
| 2914 | 2024-11-22 | 5969.339844 |
| 2915 | 2024-11-25 | 5987.370117 |
| 2916 | 2024-11-26 | 6021.629883 |
1832 rows × 2 columns
# Datos de precio de NASDAQ
df_data_nasdaq.columns
Index(['Date', 'Open', 'High', 'Low', 'Close', 'Adj Close', 'Volume'], dtype='object')
df_data_nasdaq = df_data_nasdaq.rename(columns={'Date': 'date', 'Close': 'nasdaq'})[['date', 'nasdaq']]
df_data_nasdaq = df_data_nasdaq[df_data_nasdaq['date'] >= "2017-08-17"]
df_data_nasdaq['date'] = pd.to_datetime(df_data_nasdaq['date'])
df_data_nasdaq
| date | nasdaq | |
|---|---|---|
| 1085 | 2017-08-17 | 6221.910156 |
| 1086 | 2017-08-18 | 6216.529785 |
| 1087 | 2017-08-21 | 6213.129883 |
| 1088 | 2017-08-22 | 6297.479980 |
| 1089 | 2017-08-23 | 6278.410156 |
| ... | ... | ... |
| 2912 | 2024-11-20 | 18966.140625 |
| 2913 | 2024-11-21 | 18972.419922 |
| 2914 | 2024-11-22 | 19003.650391 |
| 2915 | 2024-11-25 | 19054.839844 |
| 2916 | 2024-11-26 | 19174.300781 |
1832 rows × 2 columns
# Tipo de cambio Euro/Dólar
df_eur_usd.columns
Index(['Date', 'Open', 'High', 'Low', 'Close', 'Adj Close', 'Volume'], dtype='object')
df_eur_usd = df_eur_usd.rename(columns={'Date': 'date', 'Close': 'eur_usd'})[['date', 'eur_usd']]
df_eur_usd = df_eur_usd[df_eur_usd['date'] >= "2017-08-17"]
df_eur_usd['date'] = pd.to_datetime(df_eur_usd['date'])
df_eur_usd
| date | eur_usd | |
|---|---|---|
| 1121 | 2017-08-17 | 1.177426 |
| 1122 | 2017-08-18 | 1.171550 |
| 1123 | 2017-08-21 | 1.175627 |
| 1124 | 2017-08-22 | 1.181195 |
| 1125 | 2017-08-23 | 1.176221 |
| ... | ... | ... |
| 3013 | 2024-11-20 | 1.060760 |
| 3014 | 2024-11-21 | 1.054619 |
| 3015 | 2024-11-22 | 1.046934 |
| 3016 | 2024-11-25 | 1.047987 |
| 3017 | 2024-11-26 | 1.044430 |
1897 rows × 2 columns
# Tipo de cambio Libra/Dólar
df_gbp_usd = df_gbp_usd.rename(columns={'Date': 'date', 'Close': 'gbp_usd'})[['date', 'gbp_usd']]
df_gbp_usd = df_gbp_usd[df_gbp_usd['date'] >= "2017-08-17"]
df_gbp_usd['date'] = pd.to_datetime(df_gbp_usd['date'])
df_gbp_usd
| date | gbp_usd | |
|---|---|---|
| 1121 | 2017-08-17 | 1.289158 |
| 1122 | 2017-08-18 | 1.286505 |
| 1123 | 2017-08-21 | 1.287366 |
| 1124 | 2017-08-22 | 1.289823 |
| 1125 | 2017-08-23 | 1.282397 |
| ... | ... | ... |
| 3013 | 2024-11-20 | 1.269454 |
| 3014 | 2024-11-21 | 1.265534 |
| 3015 | 2024-11-22 | 1.258479 |
| 3016 | 2024-11-25 | 1.259382 |
| 3017 | 2024-11-26 | 1.253306 |
1897 rows × 2 columns
# Tipo de cambio Yen/Dólar
df_jpy_usd = df_jpy_usd.rename(columns={'Date': 'date', 'Close': 'jpy_usd'})[['date', 'jpy_usd']]
df_jpy_usd = df_jpy_usd[df_jpy_usd['date'] >= "2017-08-17"]
df_jpy_usd['date'] = pd.to_datetime(df_jpy_usd['date'])
df_jpy_usd
| date | jpy_usd | |
|---|---|---|
| 1121 | 2017-08-17 | 0.009091 |
| 1122 | 2017-08-18 | 0.009147 |
| 1123 | 2017-08-21 | 0.009147 |
| 1124 | 2017-08-22 | 0.009179 |
| 1125 | 2017-08-23 | 0.009113 |
| ... | ... | ... |
| 3013 | 2024-11-20 | 0.006463 |
| 3014 | 2024-11-21 | 0.006439 |
| 3015 | 2024-11-22 | 0.006484 |
| 3016 | 2024-11-25 | 0.006487 |
| 3017 | 2024-11-26 | 0.006478 |
1897 rows × 2 columns
# Unión de cada una de las variables al dataset inicial
df_inicial = pd.merge(df_inicial, df_data_federalfunds[['date', 'fed_funds_rate']], on='date', how='left')
df_inicial = pd.merge(df_inicial, df_data_sp500[['date', 'sp500']], on='date', how='left')
df_inicial = pd.merge(df_inicial, df_data_nasdaq[['date', 'nasdaq']], on='date', how='left')
df_inicial = pd.merge(df_inicial, df_eur_usd[['date', 'eur_usd']], on='date', how='left')
df_inicial = pd.merge(df_inicial, df_gbp_usd[['date', 'gbp_usd']], on='date', how='left')
df_inicial = pd.merge(df_inicial, df_jpy_usd[['date', 'jpy_usd']], on='date', how='left')
df_inicial
| date | open | high | low | close | VOL_BTC | VOL_USDT | transactions | buy_market_BTC | buy_market_USDT | total_supply | fed_funds_rate | sp500 | nasdaq | eur_usd | gbp_usd | jpy_usd | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-08-17 | 4261.48 | 4485.39 | 4200.74 | 4285.08 | 795.150377 | 3.454770e+06 | 3427 | 616.248541 | 2.678216e+06 | 1.651956e+07 | 1.16 | 2430.010010 | 6221.910156 | 1.177426 | 1.289158 | 0.009091 |
| 1 | 2017-08-18 | 4285.08 | 4371.52 | 3938.77 | 4108.37 | 1199.888264 | 5.086958e+06 | 5233 | 972.868710 | 4.129123e+06 | 1.651956e+07 | 1.16 | 2425.550049 | 6216.529785 | 1.171550 | 1.286505 | 0.009147 |
| 2 | 2017-08-19 | 4108.37 | 4184.69 | 3850.00 | 4139.98 | 381.309763 | 1.549484e+06 | 2153 | 274.336042 | 1.118002e+06 | 1.651956e+07 | 1.16 | NaN | NaN | NaN | NaN | NaN |
| 3 | 2017-08-20 | 4120.98 | 4211.08 | 4032.62 | 4086.29 | 467.083022 | 1.930364e+06 | 2321 | 376.795947 | 1.557401e+06 | 1.651956e+07 | 1.16 | NaN | NaN | NaN | NaN | NaN |
| 4 | 2017-08-21 | 4069.13 | 4119.62 | 3911.79 | 4016.00 | 691.743060 | 2.797232e+06 | 3972 | 557.356107 | 2.255663e+06 | 1.651956e+07 | 1.16 | 2428.370117 | 6213.129883 | 1.175627 | 1.287366 | 0.009147 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2654 | 2024-11-22 | 98317.12 | 99588.01 | 97122.11 | 98892.00 | 46189.309243 | 4.555537e+09 | 7271311 | 22709.668193 | 2.240635e+09 | 1.978572e+07 | 4.58 | 5969.339844 | 19003.650391 | 1.046934 | 1.258479 | 0.006484 |
| 2655 | 2024-11-23 | 98892.00 | 98908.85 | 97136.00 | 97672.40 | 24757.843670 | 2.431610e+09 | 3839138 | 11176.248700 | 1.097539e+09 | 1.978626e+07 | 4.58 | NaN | NaN | NaN | NaN | NaN |
| 2656 | 2024-11-24 | 97672.40 | 98564.00 | 95734.77 | 97900.04 | 31200.978380 | 3.034172e+09 | 4964720 | 15124.121760 | 1.471314e+09 | 1.978680e+07 | 4.58 | NaN | NaN | NaN | NaN | NaN |
| 2657 | 2024-11-25 | 97900.05 | 98871.80 | 92600.19 | 93010.01 | 50847.450960 | 4.883445e+09 | 8289691 | 23486.927050 | 2.255517e+09 | 1.978734e+07 | 4.58 | 5987.370117 | 19054.839844 | 1.047987 | 1.259382 | 0.006487 |
| 2658 | 2024-11-26 | 93010.01 | 94973.37 | 90791.10 | 91965.16 | 57858.731380 | 5.370919e+09 | 10225809 | 29120.267650 | 2.702544e+09 | 1.978734e+07 | 4.58 | 6021.629883 | 19174.300781 | 1.044430 | 1.253306 | 0.006478 |
2659 rows × 17 columns
df_inicial.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 2659 entries, 0 to 2658 Data columns (total 17 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date 2659 non-null datetime64[ns] 1 open 2659 non-null float64 2 high 2659 non-null float64 3 low 2659 non-null float64 4 close 2659 non-null float64 5 VOL_BTC 2659 non-null float64 6 VOL_USDT 2659 non-null float64 7 transactions 2659 non-null int64 8 buy_market_BTC 2659 non-null float64 9 buy_market_USDT 2659 non-null float64 10 total_supply 2659 non-null float64 11 fed_funds_rate 2659 non-null float64 12 sp500 1832 non-null float64 13 nasdaq 1832 non-null float64 14 eur_usd 1897 non-null float64 15 gbp_usd 1897 non-null float64 16 jpy_usd 1897 non-null float64 dtypes: datetime64[ns](1), float64(15), int64(1) memory usage: 353.3 KB
# Se realiza una copia para trabajar con ella y poder acudir al dataframe inicial original en caso necesario
df = df_inicial.copy()
Con el dataset inicial configurado, se puede proceder al siguiente punto.
2.3.1. Descripción de los datos
El dataset inicial constará de 2659 observaciones y 17 variables. Estas variables, excepto la fecha, son todas variables numéricas, ya que se han ido incorporando datos de precios, volúmenes y ratios. De hecho, a excepción del número de transacciones ('transactions'), de tipo int, todas estas variables numéricas son de tipo float. La variable close, que hace referencia al precio de cierre de Bitcoin para cada día, será la variable target, dado que el precio que se pretende predecir es el último precio del día.
Aunque, idealmente, un modelo de red neuronal recurrente debería entrenarse con al menos el doble de observaciones para garantizar resultados más robustos, estas 2659 observaciones representan el conjunto de datos con mayor riqueza informativa obtenido durante la fase de recopilación. Si bien la falta de datos anteriores a agosto de 2017 podría limitar la capacidad del modelo para capturar tendencias de muy largo plazo, disponer de siete años de datos diarios resulta prometedor para construir un modelo capaz de predecir el precio de Bitcoin con una precisión aceptable.
A simple vista, para los valores de SP500, NASDAQ y los distintos tipos de cambio se pueden observar que existen valores nulos. Ésto se debe a que, durante los fines de semana (se puede comprobar en el dataset que dichos valores nulos aparecen en grupos de dos, coincidiendo con sábados y domingos, y algunos festivos para SP500 y NASDAQ), los mercados financieros tradicionales permanecen cerrados, a diferencia de los mercados de criptomonedas, que operan los 7 días de la semana, incluidos festivos. Durante el análisis y la preparación de los datos, será crucial decidir cómo manejar estos valores faltantes para no perder información relevante ni introducir sesgos en el modelo.
2.3.2. Análisis de las variables
close (target):
Es el precio de cierre de una sesión (día) de Bitcoin, por lo que será la variable objetivo a predecir por el modelo. Se expresa en USDT (una stablecoin cuyo valor es prácticamente parejo al dólar).
Se expondrán a continuación las métricas del precio de cierre de Bitcoin:
univar_num_cont(df, ['close'])
| Variable | Valores Únicos | Media | Mediana | Mínimo | Máximo | Desv. Estándar | |
|---|---|---|---|---|---|---|---|
| 0 | close | 2658 | 26092.58 | 19811.66 | 3189.02 | 98892.0 | 20621.99 |
Se puede observar que, para el periodo del que se tienen datos, el precio mínimo de cierre de Bitcoin es de 3189.02, el máximo 98892, lo cual nos indica una gran amplitud, dado que en 2017 el precio aún no había sufrido su primer bullrun que llevaría a Bitcoin a tocar por primera vez los $20000.
Distribución del precio de cierre de Bitcoin:
univar_graph_num_cont(df, 'close')
univar_graph_boxplot(df, 'close')
Del histograma del precio de cierre de Bitcoin se pueden hacer dos lecturas: la primera, que el nivel de precios de Bitcoin ha permanecido bajo durante bastante tiempo en el periodo estudiado, bastante por debajo de los 20000, y que, cuanto más alto es el precio, menos tiempo permanece en el rango de precios en cuestión (es decir, a mayor precio, menor acumulación de frecuencia de precios). Y la segunda, obviando los precios más bajos donde se acumulan la mayoría de observaciones, es que existen ciertos niveles de precios donde Bitcoin tiende a permanecer más tiempo, es decir, a acumular mayor frecuencia de observaciones; éstos son los llamados puntos de soporte o techo (según el precio se encuentre alcista o bajista), y son rangos de precio que frenan caidas o subidas. Estos se pueden observar entorno a los 20000, y en menor medida en torno a 40000 y a 60000.
Aunque se podría interpretar como un sesgo hacia la derecha de los datos, esta particularidad podría estar debida más bien a las características propias de la evolución cíclica de un mercado, por lo que no se tendrán los valores por sesgados.
La conclusión es que Bitcoin tiende a estar más tiempo en rangos de precios bajos, mientras que en precios altos la volatilidad tiende a ser mayor, reduciendo el tiempo de permanencia en esos niveles.
Por otro lado, el boxplot de la variable confirma la existencia de algunos valores outliers entre los más altos. Esto tiene sentido ya que recientemente se alcanzaron por primera vez estos valores (cercanos a 100000 dólares) en una carrera muy acelerada en las útlimas semanas.
bins = range(0, int(df['close'].max()) + 10000, 10000)
df_close_group = df['close'].copy()
df_close_group['close_group'] = pd.cut(df['close'], bins=bins, right=False)
group_counts = df_close_group['close_group'].value_counts().sort_index()
group_counts
close [0, 10000) 911 [10000, 20000) 426 [20000, 30000) 385 [30000, 40000) 248 [40000, 50000) 259 [50000, 60000) 162 [60000, 70000) 230 [70000, 80000) 21 [80000, 90000) 5 [90000, 100000) 12 Name: count, dtype: int64
Al agrupar los precios de cierre de Bitcoin, se confirma que tiende a estar más tiempo en precios bajos, concretamente en el rango de 0 a 10000.
date:
Para la variable de fecha, se comprobará el rango de las fechas y que no existen fechas faltantes en dicho rango.
print(f"Rango de fechas: {df['date'].min()} a {df['date'].max()}")
print(f"Frecuencia de observaciones: {df['date'].diff().dropna().value_counts()}")
Rango de fechas: 2017-08-17 00:00:00 a 2024-11-26 00:00:00 Frecuencia de observaciones: date 1 days 2658 Name: count, dtype: int64
El número total de días que existen entre las fechas límites es el siguiente:
abs(df['date'].max() - df['date'].min()).days +1
2659
Relación entre la variable fecha y la variable target (serie temporal de close):
time_line(df)
De la representación de la evolución del precio de cierre de Bitcoin en el tiempo se puede confirmar lo que se adelantó con el histograma del precio de cierre: el precio tiende a estar más tiempo en valores bajos (desde 2017 hasta finales de 2020 estuvo por debajo de 20000, concretamente en un rango entre los 5000 y los 12000); también se pueden observar los soportes y resistencias mencionados en 20000, 40000 y 60000, siendo los precios cercanos a ellos donde Bitcoin tiende a frenar tanto en las subidas (resistencia) como en las caídas (soporte).
Por ejemplo, se puede observar el comportamiento de Bitcoin en torno a los 20000, en el primer bullrun histórico de Bitcoin, el precio alcanzó este precio, el cual ofreció resistencia y Bitcoin fue incapaz de superarlo. Más tarde, a finales de 2020, justo antes de empezar el segundo bullrun histórico, el mercado volvió a ofrecer resistencia en 20000, mientras que en 2022, en el retroceso de este bullrun, los precios en torno a 20000 actuaron como soporte y resistieron la caída.
open, high y low:
Para los precios de apertura, mayor y menor, el estudio será similar al realizado con el precio de cierre.
univar_num_cont(df, ['open', 'high', 'low'])
| Variable | Valores Únicos | Media | Mediana | Mínimo | Máximo | Desv. Estándar | |
|---|---|---|---|---|---|---|---|
| 0 | open | 2657 | 26059.54 | 19803.30 | 3188.01 | 98892.00 | 20586.69 |
| 1 | high | 2590 | 26698.73 | 20208.37 | 3276.50 | 99588.01 | 21057.72 |
| 2 | low | 2591 | 25378.98 | 19291.75 | 2817.00 | 97136.00 | 20111.94 |
Los datos de las métricas son muy similares entre ellos y a los de close, algo que parece lógico. Pero resulta llamativo que para high y low existan algunos valores únicos menos (para open, al igual que para close, los valores únicos coinciden con el número de observaciones, es decir, todos sus valores son distintos). Este hecho se debe a la existencia de los mencionados soportes y resistencias, valores de precios que suelen ser difíciles de romper y sobrepasar. Que haya menos valores únicos para estas dos variables significa que, en diferentes ocasiones, durante varios días seguidos, si bien el precio puede tener valores de apertura y cierre diferentes, ha sido incapaz de romper un soporte o una resistencia, marcando durante esos días el mismo valor de high o low.
Que las métricas de high sean superiores a las de low, y las de open se encuentren entre medias, nos da una idea de que los valores pueden no tener fallos, aunque no lo confirma. En caso de tener un valor mínimo de high inferior al valor mínimo de low, entonces sí que habría algún fallo en los datos que debería ser estudiado y corregido, pero, en principio, parece que los datos están acorde a lo esperado.
time_line(df, _vars=['open', 'high', 'low'])
Como era de esperar, los valores de apertura, máximo diario y mínimo diario son muy similares entre ellos y al precio de cierre, dibujando la misma figura los 4 en la serie temporal. Ésto nos indica que, muy probablemente, tendrán una alta correlación entre ellos.
transactions:
Para la variable de transacciones, al ser de tipo int, se calcularán métricas descriptivas y se representará la distribución de sus datos mediante un histograma.
univar_num_cont(df, ['transactions'])
| Variable | Valores Únicos | Media | Mediana | Mínimo | Máximo | Desv. Estándar | |
|---|---|---|---|---|---|---|---|
| 0 | transactions | 2657 | 1564455.45 | 919185.0 | 2153 | 15223589 | 2000917.32 |
univar_graph_num_cont(df, 'transactions', 100)
A continuación, se estudiará la variable transactions junto a la variable target:
bivar_graph_scatt(df, 'transactions')
De las métricas de la variable transactions se puede deducir que, aunque transactions es técnicamente una variable de tipo entero y discreta, su amplio rango de valores (mínimo de 2153 y máximo superior a 15 millones) junto con una alta cantidad de valores únicos (2657 de 2659 posibles) la hace comportarse como una variable continua en la práctica.
El histograma de transactions muestra una distribución fuertemente sesgada hacia la derecha, indicando que la mayoría de los días presentan un número bajo de transacciones, con algunos valores atípicos que representan días excepcionales de alta actividad. Este comportamiento sugiere que los días con muchas transacciones son eventos menos comunes, posiblemente asociados a momentos de alta volatilidad o a eventos específicos del mercado.
Al analizar la relación entre transactions y close, se observa que la correlación no es alta. Aunque la cantidad de transacciones tiende a influir en el precio de Bitcoin, esta relación no parece lineal ni consistente. La mayoría de las transacciones ocurren cuando el precio está en un rango moderado (alrededor de 20,000), pero también existen valores atípicos con precios altos (mayores a 80,000) y niveles elevados de transacciones, aunque son menos frecuentes.
La conclusión que se puede obtener es que, por lo general, el número de transacciones es bajo, a excepción de cuando el precio está en torno a 20000, en cuyo caso las transacciones se disparan.
Sería interesante estudiar si el número de transacciones aumenta o disminuye cuando lo hace el volumen de Bitcoin negociado en el día.
bivar_graph_scatt(df, 'VOL_BTC', 'transactions')
La gráfica anterior confirma que la relación entre transactions y el volumen de Bitcoin negociado diariamente es de una tendencia positiva: a mayor volumen negociado, mayor número de transacciones. Esto sugiere que la actividad de compra-venta es un motor directo del número de transacciones en la red o en el exchange. Sin embargo, hay excepciones: algunos días con un volumen negociado moderado presentan un número elevado de transacciones, lo que podría indicar un predominio de operaciones pequeñas durante esos días.
VOL_BTC y VOL_USDT:
Las siguientes variables a estudiar son las referentes a los volúmenes negociados diariamente, tanto en Bitcoin como en USDT.
univar_num_cont(df, ['VOL_BTC', 'VOL_USDT'])
| Variable | Valores Únicos | Media | Mediana | Mínimo | Máximo | Desv. Estándar | |
|---|---|---|---|---|---|---|---|
| 0 | VOL_BTC | 2659 | 6.857641e+04 | 4.378295e+04 | 228.11 | 7.607054e+05 | 8.049931e+04 |
| 1 | VOL_USDT | 2659 | 1.701214e+09 | 9.711511e+08 | 977865.73 | 1.746531e+10 | 1.997454e+09 |
Histogramas de los volúmenes:
for var in ['VOL_BTC', 'VOL_USDT']:
univar_graph_num_cont(df, var, 100)
Boxplots para comprobar la distribución de los cuartiles y la existencia teórica de valores outliers:
for var in ['VOL_BTC', 'VOL_USDT']:
univar_graph_boxplot(df, var)
Evolución temporal de los volúmenes:
time_line(df, _vars=['VOL_BTC'])
time_line(df, _vars=['VOL_USDT'])
Análisis bivariante de los volúmenes frente al precio de cierre:
for var in ['VOL_BTC', 'VOL_USDT']:
bivar_graph_scatt(df, var)
Tanto para el volumen de Bitcoin como para el de USDT, la amplitud entre el valor mínimo y el máximo es considerable, así como la desviación estándar de ambos. De ambos gráficos de distribución se puede observar que también existe un fuerte sesgo, con una asimetría a la derecha que deja la mediana a la izquierda de la media. Aunque generalmente esto es indicativo de la existencia de posibles valores outliers (confirmado por los gráficos boxplots), puede ser algo normal por la naturaleza de los activos financieros, ya que ciertos períodos de alta volatilidad (como bullruns o caídas dramáticas) suelen concentrar la actividad del mercado, así como por algunos eventos extremos y poco frecuentes.
De hecho, si se comprueba la evolución en el tiempo de ambos volúmenes, se puede ver que los valores atípicos se corresponden, casi todos ellos con momentos de mucha eufória (como en el bullrun de 2021 y la lucha por resistir el soporte de los 15000 de finales de 2022). También se pueden advertir otros momentos de volumen atípico motivados por sucesos a nivel mundial externos al mundo de las criptomonedas pero que afectan a su precio, como el inicio de la pandemia COVID en marzo de 2020 que provocó una gran cantidad de liquidaciones y movimientos defensivos.
Finalmente, de la comparación del volumen de BTC con su precio se deduce que volumen tiende a ser menor a 150000 para prácticamente todos los precios de Bitcoin, a excepción de la zona de precios cercana a 20000, donde se han producido volúmenes muy superiores a lo normal. En el caso del volumen de USDT y precio de BTC se produce un hecho similar, si bien se puede vislumbrar (siempre que se obvien los outliers) una ligera correlación positiva, aunque con una pendiente muy pronunciada. El sentido de esta relación puede deberse a que, a mayor precio de Bitcoin, mayor cantidad de USDT se necesita para negociar, incluso cuando se negocian menor cantidad de BTC.
Como conclusión, se puede confirmar que el comportamiento del volumen de Bitcoin y USDT muestra patrones consistentes con la alta volatilidad y naturaleza especulativa del mercado de criptomonedas. Los eventos globales y los niveles de soporte psicológico (como, por ejemplo, los precios cercanos a 20000 USDT) juegan un papel importante en los picos de volumen, mientras que la relación positiva entre volumen de USDT y precio de Bitcoin sugiere que mayores precios requieren mayor liquidez para facilitar la negociación. Se destaca la importancia del volumen como indicador para anticipar o confirmar cambios en la tendencia del mercado.
buy_market_BTC y buy_market_USDT:
Las siguientes variables a estudiar son las referentes a las compras a mercado. Estas suponen el número de compras que se realizaron directamente a mercado, sin establecer un límite de precio, es decir, ejecutando la orden inmediatamente al mejor precio disponible en ese momento. Este tipo de órdenes son muy comunes ya que la mayoría de traders suele preferir asegurar la ejecución inmediata en lugar de arriesgarse a no completar su orden debido a fluctuaciones rápidas de precio.
Se espera encontrar una fuerte correlación entre ambas, dado que miden el mismo fenómeno en diferentes unidades. En todo caso, no será hasta el análisis bivariante que se calculará esta posible correlación.
univar_num_cont(df, ['buy_market_BTC', 'buy_market_USDT'])
| Variable | Valores Únicos | Media | Mediana | Mínimo | Máximo | Desv. Estándar | |
|---|---|---|---|---|---|---|---|
| 0 | buy_market_BTC | 2659 | 3.409902e+04 | 2.185593e+04 | 56.19 | 3.747756e+05 | 4.005416e+04 |
| 1 | buy_market_USDT | 2659 | 8.430033e+08 | 4.707145e+08 | 241363.80 | 8.783916e+09 | 9.933625e+08 |
Histogramas de las compras a mercado:
for var in ['buy_market_BTC', 'buy_market_USDT']:
univar_graph_num_cont(df, var, 100)
Boxplots de las compras a mercado:
for var in ['buy_market_BTC', 'buy_market_USDT']:
univar_graph_boxplot(df, var)
Evolución temporal de las compras a mercado:
time_line(df, _vars=['buy_market_BTC'])
time_line(df, _vars=['buy_market_USDT'])
Análisis bivariante de las compras a mercado frente al precio de cierre:
for var in ['buy_market_BTC', 'buy_market_USDT']:
bivar_graph_scatt(df, var)
Tanto las métricas como los gráficos de ambas variables son prácticamente idénticos a los obtenidos para los correspondientes volúmenes, algo que tiene mucha lógica dado que las compras a mercado en BTC están contenidas dentro del volumen total negociado en BTC y las compras a mercado en USDT están contenidas dentro del volumen total negociado en USDT. Esto implica que es muy probable una alta correlación entre cada par de variables, si bien, las variables de compra a mercado pueden tener un valor agregado en modelos predictivos si capturan un subconjunto específico del volumen que puede ser relevante para ciertas dinámicas del mercado, como la presión de compra.
Las compras a mercado pueden tener un impacto más directo en la dinámica del precio debido a su naturaleza de ejecución inmediata, lo que podría justificar su inclusión en caso de que aporten valor predictivo adicional.
total_supply:
Es el suministro circulante de Bitcoin, es decir, la cantidad de Bitcoin que se encuentra disponible en el mercado para negociar. Se trata de una medida clave de la cantidad total de Bitcoin disponible en el mercado, y su crecimiento es predecible debido al límite máximo de 21 millones. La escasez de Bitcoin tiene un impacto directo en su precio: a medida que más BTC están en circulación, se vuelve más difícil minar nuevos bloques (debido a la recompensa decreciente por el halving), lo que a menudo provoca un aumento en la demanda y el precio, sobre todo si la adopción crece.
univar_num_cont(df, ['total_supply'])
| Variable | Valores Únicos | Media | Mediana | Mínimo | Máximo | Desv. Estándar | |
|---|---|---|---|---|---|---|---|
| 0 | total_supply | 2654 | 18495452.87 | 18676390.25 | 16519562.0 | 19787340.0 | 935805.23 |
Histograma:
univar_graph_num_cont(df, 'total_supply', 100)
Evolución temporal:
time_line(df, _vars=['total_supply'])
Boxplot:
univar_graph_boxplot(df, 'total_supply')
Análisis bivariante entre _totalsupply y close:
bivar_graph_scatt(df, 'total_supply')
Se puede comprobar que los valores mínimo y máximo del suministro total de BTC coinciden con los valores para las dos fechas más extremas de la serie temporal, lo que se deduce del carácter acumulativo de esta variable. En el histograma se puede interpretar este hecho de una manera diferente, introduciendo además el halving: se puede observar que existen 2 saltos en las frecuencias, uno en el entorno de 1.85e+07 de supply y otro pasado el 1.95e+07, de forma que las frecuencias son mayores dado que es más difícil minar nuevos Bitcoins y, por tanto, el supply aumenta más lentamente, lo que implica que permanece en el mismo nivel durante más tiempo.
La evolución temporal del supply confirma este hecho con dos momentos en los que la línea suaviza su pendiente: uno en 2020 (11/05/2020) en el entorno de 1.85e+07 de supply, y otro en 2024 (19/04/2024) pasado el 1.95e+07 de supply.
Además, el gráfico boxplot confirma que no existen datos outliers en esta variable.
Finalmente, dado que _totalsupply es una variable acumulativa que crece en el tiempo, al relacionarla con el precio se obtiene una figura muy similar a la evolución temporal del precio. Esto sugiere que no exite una relación lineal fuerte entre ambas variables, si bien podría existir una relación de otro tipo. Además, la inclusión de esta variable podría proporcionar contexto al modelo a entrenar, de forma que, si bien no ayude de forma directa a predecir el precio, pueda ayudar al modelo a entender de qué manera el aumentar el supply afecta a la velocidad a la que se producen las subidas y caídas del precio.
fed_funds_rate:
La federal funds rate es la tasa de interés de referencia de los fondos federales de Estados Unidos, empleada por los bancos cuando prestan dinero en la Reserva Federal a otras instituciones depositarias de un día para otro. Es decir, es la tasa de interés que los bancos se cobran entre sí cuando se prestan dinero. El nivel de este interés afecta a las inversiones financieras (entre ellas, inversiones en Bitcoin), ya que una política de tasas de interés bajas puede impulsar la inversión financiera, ya que los rendimientos de los bonos pueden volverse menos atractivos. Además, las decisiones de la FED (la Reserva Federal de Estados Unidos) pueden influir en la percepción de riesgo en los mercados, lo que puede llevar a movimientos bruscos en los precios.
La variable se expresa en tanto por ciento.
univar_num_disc(df, ['fed_funds_rate'])
| Variable | Mínimo | Perc25 | Mediana | Perc75 | Máximo | Moda | Desv. Estándar | |
|---|---|---|---|---|---|---|---|---|
| 0 | fed_funds_rate | 0.04 | 0.1 | 1.85 | 4.33 | 5.33 | 5.33 | 1.94 |
Histograma de _fed_fundsrate:
univar_graph_num_cont(df, 'fed_funds_rate', 100)
Boxplot:
univar_graph_boxplot(df, 'fed_funds_rate')
Evolución en el tiempo:
time_line(df, _vars=['fed_funds_rate'])
Análisis bivariante entre _fed_fundsrate y close:
bivar_graph_scatt(df, 'fed_funds_rate')
De las métricas básicas de esta variable se pueden extraer las siguientes conclusiones:
En el gráfico de distribución se puede observar la distribución irregular de la variable. Cabe destacar que, si bien el valor moda es el máximo (5.33), la barra que acumula mayor frecuencia es la primera (la que acumula los valores más cercanos a 0), lo que implica que, si bien no siempre es el mismo ratio, los valores más frecuentes están cercanos a 0. También se puede confirmar que no existen valores atípicos según el gráfico boxplot.
La evolución en el tiempo de esta variable confirma lo que se pudo ver en el histograma: existe un periodo entre el primer trimestre de 2020 y el primer trimestre de 2022 en que el interés cayó hasta casi 0 y se mantuvo durante todo el mencionado periodo (posiblemente motivado por la crisis que sobrevino con la pandemia COVID), si bien existen leves variaciones en el entorno del 0.1%. En contraste, a mediados de 2023 culmina una escalada incesante desde 2022 y el interés alcanza el 5.33, su valor máximo en el periodo del estudio, manteniéndose inamovible hasta el tercer cuatrimestre de 2024, cuando cayó por debajo del 5%.
De la comparación entre la variable y el target se puede establecer que no parece existir relación lineal alguna, aunque es importante señalar que la ausencia de correlación lineal no implica que no haya influencia. Las relaciones con tasas de interés suelen ser no lineales o condicionales (por ejemplo, tasas bajas pueden correlacionarse con mercados alcistas, pero tasas altas no necesariamente implican mercados bajistas). En cualquier caso, dado que se conoce que los movimientos de los tipos de interés influyen en las inversiones financieras, es interesante mantener esta variable para proporcionar contexto al modelo.
Por ejemplo, la caída de la tasa de interés por el COVID en 2020 coincide con una caída del precio de Bitcoin provocada por el miedo a una nueva crisis, pero pocos meses después, aún con el interés cercano a 0, BTC comenzó a recuperarse lentamente hasta tomar velocidad y marcar nuevos máximos históricos en 2021; por otro lado, el bullrun actual comenzó lentamente en 2023 y se disparó en 2024 para sufrir un retroceso y volver a dispararse de nuevo a finales de año, por lo que sería interesante que el modelo recogiera el hecho de que el bullrun comenzó lento con una tasa de interés máxima y se disparó cuando el interés bajó.
SP500 y NASDAQ:
El SP500 y el NASDAQ son dos de los índices bursátiles más importantes del mundo: SP500 es el índice más representativo de la situación real del mercado, contando la capitalización bursátil de 500 grandes empresas que poseen acciones que cotizan en las bolsas NYSE o NASDAQ, y captura aproximadamente el 80% de toda la capitalización de mercado en Estados Unidos; el NASDAQ es la segunda bolsa de valores electrónica automatizada más grande de Estados Unidos caracterizada por comprender las empresas de alta tecnología en electrónica, informática, telecomunicaciones, biotecnología, etc.
Al igual que se ha comentado con respecto a la tasa de interés de la FED, se espera que ambos mercados mantengan una relación con Bitcoin un tanto especial: sería normal encontrar una correlación positiva en momentos de crisis (pues las inversiones financieras se consideran de riesgo y los inversores tienden a refugiarse en valores seguros, como el oro) y comportarse de manera descorrelacionada en otros momentos.
univar_num_cont(df, ['sp500', 'nasdaq'])
| Variable | Valores Únicos | Media | Mediana | Mínimo | Máximo | Desv. Estándar | |
|---|---|---|---|---|---|---|---|
| 0 | sp500 | 1828 | 3757.93 | 3822.01 | 2237.40 | 6021.63 | 910.90 |
| 1 | nasdaq | 1828 | 11406.84 | 11462.24 | 6192.92 | 19298.76 | 3433.19 |
for var in ['sp500', 'nasdaq']:
univar_graph_num_cont(df, var, 100)
for var in ['sp500', 'nasdaq']:
univar_graph_boxplot(df, var)
time_line(df, _vars=['sp500', 'nasdaq'])
for var in ['sp500', 'nasdaq']:
bivar_graph_scatt(df, var)
bivar_graph_scatt(df, 'sp500', 'nasdaq')
De las métricas y el histograma se puede deducir que la distribución de los datos es relativamente equilibrada, como muestran una media y una mediana bastante cercanas entre sí y muy centradas, aunque ligeramente hacia la izquierda. Este posicionamiento hacia la izquierda sugiere un sesgo hacia la izquierda, lo que implica que hay más valores bajos que altos ya que, históricamente, los índices tuvieron más periodos de valores relativamente bajos que altos. Este fenómeno es común en mercados bursátiles debido a su crecimiento gradual a lo largo del tiempo. El sesgo hacia la izquierda no implica necesariamente caídas recurrentes, sino que refleja una tendencia a operar más tiempo en valores cercanos a niveles históricos que a máximos recientes.
Dado que es muy difícil que el precio cierre dos días en el mismo valor, se entiende que todos los valores serán únicos. Como sólo se poseen 1828 valores únicos para cada uno de ambos mercados, número inferior a los 2659 observaciones, se tendrá que tomar una determinación sobre cómo rellenar dichos valores nulos correspondientes, como ya se ha adelantado, a los fines de semana, cuando los índices bursátiles no continúan operando.
Los boxplots de ambas variables confirman la no existencia de valores outliers, dado que, a diferencia de Bitcoin (donde sí se encuentran outliers), estos mercados, cuando son alcistas, suben de manera más paulatina y con mucha menos volatilidad. Esto puede comprobarse en las series temporales representadas, donde se ve que ambos índices siguen la misma estructura alcista, con los mismos movimientos tanto al alza como en los retrocesos, aunque en diferentes escalas. En todo caso, ambos mercados se encuentran alcistas y estas subidas son más calmadas que en Bitcoin.
Finalmente, al realizar un análisis bivariante, los scatterplots de ambas variables con la variable target parecen indicar que existe cierta relación lineal, no extremadamente fuerte pero sí de cierta consistencia. Dado que las empresas más grandes de NASDAQ cotizan también en SP500, los incrementos y caídas del valor de sus acciones afecta en gran medida a ambos mercados, por lo que es fácil sospechar que podría existir una fuerte relación lineal entre ambos, algo que se confirma al visualizar el scatterplot resultante de relacionar ambos índices. A estas relaciones se les pondrá valor en el análisis multivariante y se tomará una decisión sobre si mantener estas variables en el dataset o eliminarlas.
Euro/Dólar, Libra/Dólar y Yen/Dólar:
Los tipos de cambio como Euro/Dólar (EURUSD), Libra/Dólar (GBPUSD) y Yen/Dólar (JPYUSD) afectan indirectamente el precio de Bitcoin a través de su impacto en la demanda de la criptomoneda en diferentes regiones. La fluctuación en el valor del dólar puede hacer que Bitcoin sea más caro o más barato en otras monedas, influir en las decisiones de inversión y actuar como un refugio de valor cuando las monedas fiduciarias pierden poder adquisitivo.
pairs = ['eur_usd', 'gbp_usd', 'jpy_usd']
univar_num_cont(df, pairs)
| Variable | Valores Únicos | Media | Mediana | Mínimo | Máximo | Desv. Estándar | |
|---|---|---|---|---|---|---|---|
| 0 | eur_usd | 1728 | 1.12 | 1.12 | 0.96 | 1.25 | 0.06 |
| 1 | gbp_usd | 1729 | 1.29 | 1.29 | 1.07 | 1.43 | 0.06 |
| 2 | jpy_usd | 1839 | 0.01 | 0.01 | 0.01 | 0.01 | 0.00 |
univar_data = []
univar_data.append(['jpy_usd', round(df['jpy_usd'].mean(),6), round(df['jpy_usd'].median(),6), round(df['jpy_usd'].min(),6),
round(df['jpy_usd'].max(),6), round(np.sqrt(np.var(df['jpy_usd'])),6)])
pd.DataFrame(univar_data, columns=["Variable", "Media", "Mediana", "Mínimo", "Máximo", "Desv. Estándar"])
| Variable | Media | Mediana | Mínimo | Máximo | Desv. Estándar | |
|---|---|---|---|---|---|---|
| 0 | jpy_usd | 0.008358 | 0.008907 | 0.006187 | 0.009739 | 0.001069 |
for var in pairs:
univar_graph_num_cont(df, var, 100)
for var in pairs:
univar_graph_boxplot(df, var)
time_line(df, _vars=['eur_usd', 'gbp_usd'])
time_line(df, _vars=['jpy_usd'])
for var in pairs:
bivar_graph_scatt(df, var)
for i in range(len(pairs)):
for j in range(i+1, len(pairs)):
bivar_graph_scatt(df, pairs[i], pairs[j])
Las métricas de las tres variables indican lo siguiente:
Si se visualizan las distribuciones de las variables, en el caso de eur_usd y gbp_usd se puede observar que tienen un cierto parecido a una distribución normal, con la media y la mediana bastante centradas, si bien, en ambos casos con una leve asimetría a la izquierda. En el caso de jpy_usd no existe tal parecido, acumulando la mayor parte de los valores por encima de 0.0085, estando la mediana (0.0089) bastante cercana al máximo.
El boxplot de gbp_usd nos informa de que esta variable es la única de las 3 que tiene una serie de valores outliers, concretamente entre los valores más pequeños.
La evolución en el tiempo de eur_usd y gbp_usd dibujan dos líneas muy parecidas, con mayor volatilidad en gbp_usd, pero con muy pocas diferencias. En cualquier caso, en el periodo estudiado no se puede considerar que exista una tendencia clara ni alcista ni bajista, confirmándose la lateralidad actual de ambos pares de divisas. Los valores mínimos de gbp_usd considerados como outliers fueron alcanzados en la segunda mitad de 2022, debido a una política fiscal expansiva del gobierno de Liz Truss que provocó una fuga masiva de capitales del Reino Unido, y coincidiendo con los mínimos valores del precio de BTC desde el bullrun de 2021 y sus picos máximos en volumen. Esto deja la impresión de que la confianza de los inversores afecta al precio de Bitcoin cuando alguna de las grandes divisas sufre fuertes caídas. Por otro lado, el hecho de que ambas gráficas sean tan parecidas indica que es muy probable que exista correlación entre ambas variables.
En el caso de jpy_usd, se encuentra en una tendencia negativa que comenzó a finales de 2021, cuando rompió la resistencia que venía respetando durante los años anteriores mientras se encontraba lateralizada. El hecho de que cayera tan pronto desde valores por encima de 0.0085 hasta valores en torno a 0.007 y 0.0065 explica por qué en la distribución aparece un espacio cuasi-vacío entre los valores más altos y los más bajos. Cabe destacar que la primera parte de la caída de jpy_usd ocurre en 2022, al igual que las caídas en eur_usd y gbp_usd, tocando un primer mínimo por las mismas fechas que lo hicieron los otros dos pares de divisas, para luego sufrir un retroceso en la caída (pequeño crecimiento a finales de 2022 y el inicio de 2023) para, finalmente continuar su descenso, a diferencia de eur_usd y gbp_usd.
Los gráficos scatterplot de los tres pares de divisas con relación al precio de Bitcoin nos indican que no existe relación lineal entre ninguna de ellas con BTC, si bien podría existir otro tipo de relación no lineal o dinámica (por ejemplo, con rezagos temporales) que podría ser relevante para el modelo. Pero, dado que se ha encontrado una posible relación lineal entre eur_usd y gbp_usd, se procede a presentar también los gráficos scatterplot de las variables entre sí, encontrando que se confirma la relación lineal muy fuerte entre las dos primeras y muy leve entre cada una de ellas con jpy_usd (dado que, si bien no existe una línea clara que se pueda dibujar en ninguno de los dos gráficos, es evidente que un mayor valor de eur_usd y gbp_usd se asocia con un mayor valor de jpy_usd). La conveniencia o no de incluir o descartar alguna de estas variables se decidirá cuando se calculen matemáticamente estas correlaciones durante el análisis multivariante, por ejemplo eliminando eur_usd o gbp_usd si ambas están muy correlacionadas y evitar así la multicolinealidad.
Análisis multivariante:
A continuación, se realizará un estudio de las correlaciones entre cada par de variables, tanto visual como matemático. Como se ha visto que pueden existir variables que tienen cierta relación pero ésta no es lineal, se añadirán al estudio de las correlaciones de Pearson (qué miden la relación lineal entre variables) el estudio de las correlaciones de Spearman y de Kendall.
Como umbral para considerar dos variables explicativas demasiado correladas se establecerá un coeficiente igual o superior a 0.9. En el caso de la variable target, se considerará poco correlada y, por tanto, poco explicativa, cualquier variable que obtenga con close un coeficiente de correlación entre 0.1 y -0.1 (muy cercanas a 0), por lo que sería un motivo bastante justificado como para eliminar dicha variable.
Correlaciones de Pearson:
multivar_graph_correlation_matrix(df.drop(columns=['date']))
df.drop(columns=['date']).corr()
| open | high | low | close | VOL_BTC | VOL_USDT | transactions | buy_market_BTC | buy_market_USDT | total_supply | fed_funds_rate | sp500 | nasdaq | eur_usd | gbp_usd | jpy_usd | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| open | 1.000000 | 0.999326 | 0.998931 | 0.998602 | -0.050932 | 0.403910 | 0.312310 | -0.055124 | 0.400038 | 0.730329 | 0.303723 | 0.916983 | 0.925492 | -0.169105 | 0.147354 | -0.562611 |
| high | 0.999326 | 1.000000 | 0.998691 | 0.999411 | -0.046247 | 0.411396 | 0.316357 | -0.050083 | 0.408123 | 0.727267 | 0.297860 | 0.914813 | 0.924352 | -0.163556 | 0.152149 | -0.557441 |
| low | 0.998931 | 0.998691 | 1.000000 | 0.999217 | -0.057808 | 0.392254 | 0.308673 | -0.061415 | 0.389263 | 0.735042 | 0.313374 | 0.920797 | 0.928129 | -0.177801 | 0.138865 | -0.570382 |
| close | 0.998602 | 0.999411 | 0.999217 | 1.000000 | -0.051808 | 0.402791 | 0.312901 | -0.055242 | 0.400108 | 0.730313 | 0.304584 | 0.917161 | 0.926011 | -0.168549 | 0.146795 | -0.562430 |
| VOL_BTC | -0.050932 | -0.046247 | -0.057808 | -0.051808 | 1.000000 | 0.838231 | 0.841200 | 0.999515 | 0.839792 | 0.265933 | 0.067591 | 0.067349 | 0.037945 | -0.428184 | -0.476819 | -0.217851 |
| VOL_USDT | 0.403910 | 0.411396 | 0.392254 | 0.402791 | 0.838231 | 1.000000 | 0.894138 | 0.836553 | 0.999435 | 0.494309 | 0.125324 | 0.449365 | 0.437706 | -0.350513 | -0.248227 | -0.384698 |
| transactions | 0.312310 | 0.316357 | 0.308673 | 0.312901 | 0.841200 | 0.894138 | 1.000000 | 0.841089 | 0.896259 | 0.499894 | 0.272842 | 0.434128 | 0.385135 | -0.486234 | -0.405947 | -0.474883 |
| buy_market_BTC | -0.055124 | -0.050083 | -0.061415 | -0.055242 | 0.999515 | 0.836553 | 0.841089 | 1.000000 | 0.838934 | 0.260344 | 0.071001 | 0.064005 | 0.033372 | -0.429640 | -0.478809 | -0.218185 |
| buy_market_USDT | 0.400038 | 0.408123 | 0.389263 | 0.400108 | 0.839792 | 0.999435 | 0.896259 | 0.838934 | 1.000000 | 0.491501 | 0.128230 | 0.446919 | 0.434354 | -0.353478 | -0.252387 | -0.385840 |
| total_supply | 0.730329 | 0.727267 | 0.735042 | 0.730313 | 0.265933 | 0.494309 | 0.499894 | 0.260344 | 0.491501 | 1.000000 | 0.496232 | 0.891970 | 0.876422 | -0.602098 | -0.363072 | -0.738556 |
| fed_funds_rate | 0.303723 | 0.297860 | 0.313374 | 0.304584 | 0.067591 | 0.125324 | 0.272842 | 0.071001 | 0.128230 | 0.496232 | 1.000000 | 0.477952 | 0.348750 | -0.564029 | -0.482954 | -0.842434 |
| sp500 | 0.916983 | 0.914813 | 0.920797 | 0.917161 | 0.067349 | 0.449365 | 0.434128 | 0.064005 | 0.446919 | 0.891970 | 0.477952 | 1.000000 | 0.981083 | -0.382249 | -0.059361 | -0.731359 |
| nasdaq | 0.925492 | 0.924352 | 0.928129 | 0.926011 | 0.037945 | 0.437706 | 0.385135 | 0.033372 | 0.434354 | 0.876422 | 0.348750 | 0.981083 | 1.000000 | -0.272990 | 0.030864 | -0.620690 |
| eur_usd | -0.169105 | -0.163556 | -0.177801 | -0.168549 | -0.428184 | -0.350513 | -0.486234 | -0.429640 | -0.353478 | -0.602098 | -0.564029 | -0.382249 | -0.272990 | 1.000000 | 0.874794 | 0.728541 |
| gbp_usd | 0.147354 | 0.152149 | 0.138865 | 0.146795 | -0.476819 | -0.248227 | -0.405947 | -0.478809 | -0.252387 | -0.363072 | -0.482954 | -0.059361 | 0.030864 | 0.874794 | 1.000000 | 0.524519 |
| jpy_usd | -0.562611 | -0.557441 | -0.570382 | -0.562430 | -0.217851 | -0.384698 | -0.474883 | -0.218185 | -0.385840 | -0.738556 | -0.842434 | -0.731359 | -0.620690 | 0.728541 | 0.524519 | 1.000000 |
Observando el heatmap de correlaciones y el cálculo de los coeficientes de correlación de Pearson, se advierten los siguientes puntos interesantes:
Correlaciones de Spearman:
multivar_graph_correlation_matrix(df.drop(columns=['date']), 'spearman')
df.drop(columns=['date']).corr(method='spearman')
| open | high | low | close | VOL_BTC | VOL_USDT | transactions | buy_market_BTC | buy_market_USDT | total_supply | fed_funds_rate | sp500 | nasdaq | eur_usd | gbp_usd | jpy_usd | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| open | 1.000000 | 0.999320 | 0.999030 | 0.998692 | 0.126852 | 0.753330 | 0.729066 | 0.105144 | 0.751067 | 0.818993 | 0.123365 | 0.921622 | 0.933239 | -0.254899 | 0.060717 | -0.508918 |
| high | 0.999320 | 1.000000 | 0.998690 | 0.999354 | 0.134570 | 0.757928 | 0.732426 | 0.113593 | 0.756121 | 0.816173 | 0.120736 | 0.919011 | 0.931359 | -0.252033 | 0.062989 | -0.506917 |
| low | 0.999030 | 0.998690 | 1.000000 | 0.999295 | 0.116099 | 0.747086 | 0.724745 | 0.095183 | 0.745266 | 0.823166 | 0.127820 | 0.925472 | 0.936840 | -0.260881 | 0.054895 | -0.511971 |
| close | 0.998692 | 0.999354 | 0.999295 | 1.000000 | 0.125614 | 0.752711 | 0.728645 | 0.105270 | 0.751245 | 0.819039 | 0.123524 | 0.921750 | 0.933977 | -0.254962 | 0.059737 | -0.508375 |
| VOL_BTC | 0.126852 | 0.134570 | 0.116099 | 0.125614 | 1.000000 | 0.705655 | 0.672823 | 0.997299 | 0.706506 | 0.202260 | -0.199367 | 0.147312 | 0.171478 | -0.231918 | -0.269074 | 0.093797 |
| VOL_USDT | 0.753330 | 0.757928 | 0.747086 | 0.752711 | 0.705655 | 1.000000 | 0.964506 | 0.690400 | 0.999398 | 0.715244 | 0.010746 | 0.719845 | 0.727769 | -0.383623 | -0.175055 | -0.383870 |
| transactions | 0.729066 | 0.732426 | 0.724745 | 0.728645 | 0.672823 | 0.964506 | 1.000000 | 0.656436 | 0.963143 | 0.771293 | 0.100732 | 0.731861 | 0.736457 | -0.447518 | -0.245412 | -0.415538 |
| buy_market_BTC | 0.105144 | 0.113593 | 0.095183 | 0.105270 | 0.997299 | 0.690400 | 0.656436 | 1.000000 | 0.693344 | 0.181069 | -0.194682 | 0.127987 | 0.151482 | -0.226463 | -0.268000 | 0.098868 |
| buy_market_USDT | 0.751067 | 0.756121 | 0.745266 | 0.751245 | 0.706506 | 0.999398 | 0.963143 | 0.693344 | 1.000000 | 0.712512 | 0.013300 | 0.719057 | 0.726497 | -0.385045 | -0.176763 | -0.386177 |
| total_supply | 0.818993 | 0.816173 | 0.823166 | 0.819039 | 0.202260 | 0.715244 | 0.771293 | 0.181069 | 0.712512 | 1.000000 | 0.475898 | 0.919736 | 0.880240 | -0.660933 | -0.389872 | -0.719197 |
| fed_funds_rate | 0.123365 | 0.120736 | 0.127820 | 0.123524 | -0.199367 | 0.010746 | 0.100732 | -0.194682 | 0.013300 | 0.475898 | 1.000000 | 0.296863 | 0.181164 | -0.595917 | -0.532074 | -0.701242 |
| sp500 | 0.921622 | 0.919011 | 0.925472 | 0.921750 | 0.147312 | 0.719845 | 0.731861 | 0.127987 | 0.719057 | 0.919736 | 0.296863 | 1.000000 | 0.980933 | -0.434331 | -0.091766 | -0.630511 |
| nasdaq | 0.933239 | 0.931359 | 0.936840 | 0.933977 | 0.171478 | 0.727769 | 0.736457 | 0.151482 | 0.726497 | 0.880240 | 0.181164 | 0.980933 | 1.000000 | -0.325291 | 0.000386 | -0.520488 |
| eur_usd | -0.254899 | -0.252033 | -0.260881 | -0.254962 | -0.231918 | -0.383623 | -0.447518 | -0.226463 | -0.385045 | -0.660933 | -0.595917 | -0.434331 | -0.325291 | 1.000000 | 0.849135 | 0.699499 |
| gbp_usd | 0.060717 | 0.062989 | 0.054895 | 0.059737 | -0.269074 | -0.175055 | -0.245412 | -0.268000 | -0.176763 | -0.389872 | -0.532074 | -0.091766 | 0.000386 | 0.849135 | 1.000000 | 0.409807 |
| jpy_usd | -0.508918 | -0.506917 | -0.511971 | -0.508375 | 0.093797 | -0.383870 | -0.415538 | 0.098868 | -0.386177 | -0.719197 | -0.701242 | -0.630511 | -0.520488 | 0.699499 | 0.409807 | 1.000000 |
Al analizar las correlaciones de Spearman, que miden las posibles relaciones entre variables ya sean lineales o no, se observan las siguientes diferencias con respecto a las correlaciones lineales:
Correlaciones de Kendall:
multivar_graph_correlation_matrix(df.drop(columns=['date']), 'kendall')
df.drop(columns=['date']).corr(method='kendall')
| open | high | low | close | VOL_BTC | VOL_USDT | transactions | buy_market_BTC | buy_market_USDT | total_supply | fed_funds_rate | sp500 | nasdaq | eur_usd | gbp_usd | jpy_usd | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| open | 1.000000 | 0.980122 | 0.977722 | 0.970377 | 0.083840 | 0.545186 | 0.524059 | 0.070087 | 0.542042 | 0.598582 | 0.071434 | 0.754019 | 0.773402 | -0.151143 | 0.043236 | -0.306567 |
| high | 0.980122 | 1.000000 | 0.972273 | 0.981397 | 0.090519 | 0.552035 | 0.529332 | 0.077334 | 0.549461 | 0.592718 | 0.068767 | 0.749320 | 0.770298 | -0.149335 | 0.044143 | -0.304852 |
| low | 0.977722 | 0.972273 | 1.000000 | 0.979936 | 0.074582 | 0.536089 | 0.517664 | 0.061540 | 0.533648 | 0.606606 | 0.075081 | 0.761764 | 0.780208 | -0.155780 | 0.039482 | -0.309083 |
| close | 0.970377 | 0.981397 | 0.979936 | 1.000000 | 0.082826 | 0.544230 | 0.523535 | 0.070209 | 0.542228 | 0.598671 | 0.071359 | 0.754792 | 0.775246 | -0.151749 | 0.042127 | -0.306454 |
| VOL_BTC | 0.083840 | 0.090519 | 0.074582 | 0.082826 | 1.000000 | 0.538366 | 0.514524 | 0.959477 | 0.539279 | 0.159780 | -0.133687 | 0.097198 | 0.106702 | -0.154446 | -0.184895 | 0.065543 |
| VOL_USDT | 0.545186 | 0.552035 | 0.536089 | 0.544230 | 0.538366 | 1.000000 | 0.843224 | 0.523807 | 0.979949 | 0.505818 | -0.012276 | 0.500796 | 0.516655 | -0.284773 | -0.120487 | -0.215539 |
| transactions | 0.524059 | 0.529332 | 0.517664 | 0.523535 | 0.514524 | 0.843224 | 1.000000 | 0.499546 | 0.839376 | 0.580768 | 0.038994 | 0.533696 | 0.542310 | -0.325244 | -0.168056 | -0.239088 |
| buy_market_BTC | 0.070087 | 0.077334 | 0.061540 | 0.070209 | 0.959477 | 0.523807 | 0.499546 | 1.000000 | 0.527751 | 0.146539 | -0.130326 | 0.086289 | 0.095273 | -0.149024 | -0.183126 | 0.068162 |
| buy_market_USDT | 0.542042 | 0.549461 | 0.533648 | 0.542228 | 0.539279 | 0.979949 | 0.839376 | 0.527751 | 1.000000 | 0.503286 | -0.009501 | 0.499339 | 0.514453 | -0.285258 | -0.121302 | -0.218470 |
| total_supply | 0.598582 | 0.592718 | 0.606606 | 0.598671 | 0.159780 | 0.505818 | 0.580768 | 0.146539 | 0.503286 | 1.000000 | 0.337127 | 0.780122 | 0.730083 | -0.446921 | -0.228691 | -0.516758 |
| fed_funds_rate | 0.071434 | 0.068767 | 0.075081 | 0.071359 | -0.133687 | -0.012276 | 0.038994 | -0.130326 | -0.009501 | 0.337127 | 1.000000 | 0.201896 | 0.128992 | -0.414185 | -0.378449 | -0.516731 |
| sp500 | 0.754019 | 0.749320 | 0.761764 | 0.754792 | 0.097198 | 0.500796 | 0.533696 | 0.086289 | 0.499339 | 0.780122 | 0.201896 | 1.000000 | 0.896358 | -0.279059 | -0.053879 | -0.405230 |
| nasdaq | 0.773402 | 0.770298 | 0.780208 | 0.775246 | 0.106702 | 0.516655 | 0.542310 | 0.095273 | 0.514453 | 0.730083 | 0.128992 | 0.896358 | 1.000000 | -0.228552 | -0.008476 | -0.332174 |
| eur_usd | -0.151143 | -0.149335 | -0.155780 | -0.151749 | -0.154446 | -0.284773 | -0.325244 | -0.149024 | -0.285258 | -0.446921 | -0.414185 | -0.279059 | -0.228552 | 1.000000 | 0.664521 | 0.489179 |
| gbp_usd | 0.043236 | 0.044143 | 0.039482 | 0.042127 | -0.184895 | -0.120487 | -0.168056 | -0.183126 | -0.121302 | -0.228691 | -0.378449 | -0.053879 | -0.008476 | 0.664521 | 1.000000 | 0.270715 |
| jpy_usd | -0.306567 | -0.304852 | -0.309083 | -0.306454 | 0.065543 | -0.215539 | -0.239088 | 0.068162 | -0.218470 | -0.516758 | -0.516731 | -0.405230 | -0.332174 | 0.489179 | 0.270715 | 1.000000 |
Al analizar las correlaciones de Kendall, que miden las relaciones monótonas de manera más robusta frente a valores extremos que Spearman, se observan las siguientes diferencias:
Como conclusión al estudio de las correlaciones, tanto para relaciones lineales como para relaciones monótonas, se puede afirmar lo siguiente:
2.4.1. Valores nulos
Anteriormente se ha comprobado la existencia de valores nulos en algunas variables, concretamente para sp500 y nasdaq por un lado, y para _eurusd, _gbpusd y _jpyusd por otro. Para las variables sobre índices bursátiles existen 827 valores nulos y para las variables sobre pares de divisas existen 762 valores nulos.
df.isnull().sum()
date 0 open 0 high 0 low 0 close 0 VOL_BTC 0 VOL_USDT 0 transactions 0 buy_market_BTC 0 buy_market_USDT 0 total_supply 0 fed_funds_rate 0 sp500 827 nasdaq 827 eur_usd 762 gbp_usd 762 jpy_usd 762 dtype: int64
En el caso de las variables sobre pares de divisas, aunque FOREX opera los siete días de la semana, muchos proveedores de datos no actualizan sus series temporales de divisas durante el fin de semana, normalmente debido a que suelen basarse en los mercados bursátiles tradicionales, como SP500 y NASDAQ, que sí permanecen cerrados durante el fin de semana. Este parece ser el caso de la biblioteca de Yahoo Finance, de la que se han obtenido los datos de los pares de divisas.
En el caso de las variables sobre índices bursátiles, como se ha comentado, los mercados bursátiles tradicionales cierran los fines de semana, por lo que también faltan dichos datos en el dataset. La diferencia que existe entre ambos (mayor número de valores nulos en las variables sobre índices bursátiles que en las variables sobre pares de divisas) se debe a que, además de los fines de semana, los mercados tradicionales americanos permanecen cerrados en ciertas festividades nacionales, como el Día del Trabajo, el Día de Acción de Gracias o el Día de Navidad, entre otros. Durante estos días festivos en Estados Unidos, FOREX continúa operando y los proveedores de datos sí ofrecen dichas cotizaciones.
Si se fuerza a mostrar aquellas observaciones que cumplen simultáneamente que sp500 y nasdaq son nulos, se puede comprobar que se obtienen las 827 observaciones que para ambos son nulas, lo que significa que los 827 valores nulos de ambos son exactamente en las mismas fechas.
Fechas para las que sp500 y nasdaq son nulos:
df[df['sp500'].isnull() & df['nasdaq'].isnull()][['date', 'sp500', 'nasdaq']]
| date | sp500 | nasdaq | |
|---|---|---|---|
| 2 | 2017-08-19 | NaN | NaN |
| 3 | 2017-08-20 | NaN | NaN |
| 9 | 2017-08-26 | NaN | NaN |
| 10 | 2017-08-27 | NaN | NaN |
| 16 | 2017-09-02 | NaN | NaN |
| ... | ... | ... | ... |
| 2642 | 2024-11-10 | NaN | NaN |
| 2648 | 2024-11-16 | NaN | NaN |
| 2649 | 2024-11-17 | NaN | NaN |
| 2655 | 2024-11-23 | NaN | NaN |
| 2656 | 2024-11-24 | NaN | NaN |
827 rows × 3 columns
Lo mismo ocurre con las tres variables sobre pares de divisas: cada una tiene 762 valores nulos, y si se fuerza a que se muestren sólo las observaciones que cumplen que las tres son nulas, se comprueba que se obtienen 762 observaciones. Por tanto, al igual que pasaba con sp500 y nasdaq, todos los valores nulos aparecen en las mismas fechas para estas tres variables.
Fechas para las que _eurusd, _gbpusd y _jpyusd son nulos:
df[df['eur_usd'].isnull() & df['gbp_usd'].isnull() & df['jpy_usd'].isnull()][['date', 'eur_usd', 'gbp_usd', 'jpy_usd']]
| date | eur_usd | gbp_usd | jpy_usd | |
|---|---|---|---|---|
| 2 | 2017-08-19 | NaN | NaN | NaN |
| 3 | 2017-08-20 | NaN | NaN | NaN |
| 9 | 2017-08-26 | NaN | NaN | NaN |
| 10 | 2017-08-27 | NaN | NaN | NaN |
| 16 | 2017-09-02 | NaN | NaN | NaN |
| ... | ... | ... | ... | ... |
| 2642 | 2024-11-10 | NaN | NaN | NaN |
| 2648 | 2024-11-16 | NaN | NaN | NaN |
| 2649 | 2024-11-17 | NaN | NaN | NaN |
| 2655 | 2024-11-23 | NaN | NaN | NaN |
| 2656 | 2024-11-24 | NaN | NaN | NaN |
762 rows × 4 columns
Finalmente, si se observan las fechas en que se diferencian las variables sobre índices bursátiles (pues son nulas) y las variables sobre pares de divisas (pues tienen datos), se puede comprobar fácilmente que son fechas señaladas como festividades en los Estados Unidos. Por ejemplo, el 02/09/2024 fue el Día del Trabajo (Labor Day) o el 25/12/2017 fue Navidad (Christmas Day), confirmándose que estas diferencias se deben a ciertas festividades.
df[df['sp500'].isnull() & df['nasdaq'].isnull() \
& pd.notna(df['eur_usd']) & pd.notna(df['gbp_usd']) & pd.notna(df['jpy_usd'])] \
[['date', 'sp500', 'nasdaq', 'eur_usd', 'gbp_usd', 'jpy_usd']]
| date | sp500 | nasdaq | eur_usd | gbp_usd | jpy_usd | |
|---|---|---|---|---|---|---|
| 18 | 2017-09-04 | NaN | NaN | 1.188284 | 1.296512 | 0.009108 |
| 98 | 2017-11-23 | NaN | NaN | 1.181474 | 1.331682 | 0.008984 |
| 130 | 2017-12-25 | NaN | NaN | 1.185607 | 1.336505 | 0.008826 |
| 137 | 2018-01-01 | NaN | NaN | 1.200495 | 1.351607 | 0.008876 |
| 151 | 2018-01-15 | NaN | NaN | 1.219230 | 1.373306 | 0.009001 |
| ... | ... | ... | ... | ... | ... | ... |
| 2416 | 2024-03-29 | NaN | NaN | 1.079447 | 1.262626 | 0.006603 |
| 2475 | 2024-05-27 | NaN | NaN | 1.084763 | 1.273610 | 0.006375 |
| 2498 | 2024-06-19 | NaN | NaN | 1.074229 | 1.270745 | 0.006334 |
| 2513 | 2024-07-04 | NaN | NaN | 1.079331 | 1.274778 | 0.006187 |
| 2573 | 2024-09-02 | NaN | NaN | 1.104484 | 1.312853 | 0.006831 |
67 rows × 6 columns
Teniendo en cuenta que para sp500 y nasdaq los fines de semana y festivos se paraliza la cotización, el último precio de cierre anterior, es decir, los viernes o víspera de festivo, es el precio de cierre que se mantiene para estos días. Como no se desea perder la información que estas observaciones poseen para el resto de variables, se descarta la eliminación de las observaciones con valores nulos, y la decisión para rellenar los valores faltantes en estas dos variables será el mantener el último valor de cierre inmediatamente anterior a cada valor faltante. A pesar de que podría introducir inconsistencias en los datos, el perjucio por esto se considera muy inferior al beneficio de mantener dichas observaciones en el dataset.
df[['sp500', 'nasdaq']] = impute_forward_fill(df, ['sp500', 'nasdaq'])
Para el caso de _eurusd, _gbpusd y _jpyusd, dado que las cotización no se paralizan en fines de semana, no tiene sentido imputar el valor último conocido anterior, siendo una opción mucho más acertada la interpolación lineal para obtener unas aproximaciones que no supongan la "congelación" del precio durante el fin de semana. Aunque no sean valores reales sino unas aproximaciones, el sesgo introducido debería ser menor que mediante el método de imputación empleado con 'sp500' y 'nasdaq'.
df[['eur_usd', 'gbp_usd', 'jpy_usd']] = impute_interpolate(df, ['eur_usd', 'gbp_usd', 'jpy_usd'])
df[['date', 'sp500', 'nasdaq', 'eur_usd', 'gbp_usd', 'jpy_usd']]
| date | sp500 | nasdaq | eur_usd | gbp_usd | jpy_usd | |
|---|---|---|---|---|---|---|
| 0 | 2017-08-17 | 2430.010010 | 6221.910156 | 1.177426 | 1.289158 | 0.009091 |
| 1 | 2017-08-18 | 2425.550049 | 6216.529785 | 1.171550 | 1.286505 | 0.009147 |
| 2 | 2017-08-19 | 2425.550049 | 6216.529785 | 1.172909 | 1.286792 | 0.009147 |
| 3 | 2017-08-20 | 2425.550049 | 6216.529785 | 1.174268 | 1.287079 | 0.009147 |
| 4 | 2017-08-21 | 2428.370117 | 6213.129883 | 1.175627 | 1.287366 | 0.009147 |
| ... | ... | ... | ... | ... | ... | ... |
| 2654 | 2024-11-22 | 5969.339844 | 19003.650391 | 1.046934 | 1.258479 | 0.006484 |
| 2655 | 2024-11-23 | 5969.339844 | 19003.650391 | 1.047285 | 1.258780 | 0.006485 |
| 2656 | 2024-11-24 | 5969.339844 | 19003.650391 | 1.047636 | 1.259081 | 0.006486 |
| 2657 | 2024-11-25 | 5987.370117 | 19054.839844 | 1.047987 | 1.259382 | 0.006487 |
| 2658 | 2024-11-26 | 6021.629883 | 19174.300781 | 1.044430 | 1.253306 | 0.006478 |
2659 rows × 6 columns
Finalmente, se comprueba que no existen más valores nulos en el dataset.
df.isna().sum()
date 0 open 0 high 0 low 0 close 0 VOL_BTC 0 VOL_USDT 0 transactions 0 buy_market_BTC 0 buy_market_USDT 0 total_supply 0 fed_funds_rate 0 sp500 0 nasdaq 0 eur_usd 0 gbp_usd 0 jpy_usd 0 dtype: int64
2.4.2. Registros duplicados
El siguiente paso en la comprobación de la calidad de los datos es asegurarse de que no existen registros duplicados. Para ello, se procede a la eliminación de cualquier observación que aparezca duplicada mediante la función _dropduplicates(), que eliminará tantas observaciones repetidas como existan hasta asegurarse de que sólo permanece una única de ellas en el dataset. Cuando se estudió la variable date se comprobó que el número de días entre las fechas límite era de 2659, lo que coincide con el número total de observaciones, por lo que no deberían de existir ni días faltantes ni observaciones duplicadas.
print(f"Número de registros actual: {max(df.count())}")
df = df.drop_duplicates()
print(f"Número de registros tras eliminación de duplicados: {max(df.count())}")
Número de registros actual: 2659 Número de registros tras eliminación de duplicados: 2659
Como se sospechaba, no se ha eliminado ninguna observación, por lo que el dataset se encontraba libre de datos duplicados.
Durante el análisis de las variables, las gráficas boxplot han avisado de valores outliers en las variables close, _VOLBTC, _VOLUSDT, _buy_marketBTC, _buy_marketUSDT y _gbpusd.
En una serie temporal, cada observación está intrínsecamente relacionada con las anteriores y las siguientes, por lo que la eliminación de datos intermedios puede generar lagunas que dificulten al modelo el entendimiento correcto de los datos. En el contexto concreto de los mercados financieros, un outlier no indica necesariamente un error, sino que puede reflejar eventos significativos como alta volatilidad, noticias relevantes o cambios abruptos en la oferta y/o demanda. Por lo tanto, estos outliers pueden ser cruciales para entender el comportamiento del mercado y pueden aportar valor en la fase de modelado. Además, el principal modelo que se pretende entrenar es una red neuronal recurrente, que suele ser robusta ante la presencia de estos valores.
Por lo tanto, se decide optar por una estrategia diferente a la eliminación de las observaciones con datos outliers, como es la normalización por z-score. De esta manera se evita la eliminación pero se atenúa su impacto. Además, se aplicará dentro de ventanas de tiempo mensual para que el modelo pueda aprender mejor las dinámicas a lo largo del tiempo, enfocándose en patrones a corto plazo y reduciendo la influencia de cambios bruscos.
Esta normalización se llevará a cabo en la fase de transformación de variables, puesto que es necesario realizar las transformaciones antes de normalizar.
Durante el análisis de las variables se han encontrado varias con altos coeficientes de correlación (lineal o monótona), siendo la correlación entre variables explicativas una de las principales causas para descartar alguna de ellas. En este caso concreto, se ha justificado la no eliminación de varias de ellas (como las variables de precio de Bitcoin, muy correladas entre sí, y las variables de volumen y compras a mercado, pues todas ellas serán transformadas para eliminar la multicolinealidad y, al mismo, mantener la riqueza informativa del dataset.
La única variable que se eliminará en esta fase será nasdaq, pues su correlación con sp500 es de 0.98 (Coeficiente de Pearson), y ya se justificó anteriormente la permanencia de sp500 sobre ésta.
df.drop(columns=['nasdaq'], inplace=True)
df
| date | open | high | low | close | VOL_BTC | VOL_USDT | transactions | buy_market_BTC | buy_market_USDT | total_supply | fed_funds_rate | sp500 | eur_usd | gbp_usd | jpy_usd | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-08-17 | 4261.48 | 4485.39 | 4200.74 | 4285.08 | 795.150377 | 3.454770e+06 | 3427 | 616.248541 | 2.678216e+06 | 1.651956e+07 | 1.16 | 2430.010010 | 1.177426 | 1.289158 | 0.009091 |
| 1 | 2017-08-18 | 4285.08 | 4371.52 | 3938.77 | 4108.37 | 1199.888264 | 5.086958e+06 | 5233 | 972.868710 | 4.129123e+06 | 1.651956e+07 | 1.16 | 2425.550049 | 1.171550 | 1.286505 | 0.009147 |
| 2 | 2017-08-19 | 4108.37 | 4184.69 | 3850.00 | 4139.98 | 381.309763 | 1.549484e+06 | 2153 | 274.336042 | 1.118002e+06 | 1.651956e+07 | 1.16 | 2425.550049 | 1.172909 | 1.286792 | 0.009147 |
| 3 | 2017-08-20 | 4120.98 | 4211.08 | 4032.62 | 4086.29 | 467.083022 | 1.930364e+06 | 2321 | 376.795947 | 1.557401e+06 | 1.651956e+07 | 1.16 | 2425.550049 | 1.174268 | 1.287079 | 0.009147 |
| 4 | 2017-08-21 | 4069.13 | 4119.62 | 3911.79 | 4016.00 | 691.743060 | 2.797232e+06 | 3972 | 557.356107 | 2.255663e+06 | 1.651956e+07 | 1.16 | 2428.370117 | 1.175627 | 1.287366 | 0.009147 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2654 | 2024-11-22 | 98317.12 | 99588.01 | 97122.11 | 98892.00 | 46189.309243 | 4.555537e+09 | 7271311 | 22709.668193 | 2.240635e+09 | 1.978572e+07 | 4.58 | 5969.339844 | 1.046934 | 1.258479 | 0.006484 |
| 2655 | 2024-11-23 | 98892.00 | 98908.85 | 97136.00 | 97672.40 | 24757.843670 | 2.431610e+09 | 3839138 | 11176.248700 | 1.097539e+09 | 1.978626e+07 | 4.58 | 5969.339844 | 1.047285 | 1.258780 | 0.006485 |
| 2656 | 2024-11-24 | 97672.40 | 98564.00 | 95734.77 | 97900.04 | 31200.978380 | 3.034172e+09 | 4964720 | 15124.121760 | 1.471314e+09 | 1.978680e+07 | 4.58 | 5969.339844 | 1.047636 | 1.259081 | 0.006486 |
| 2657 | 2024-11-25 | 97900.05 | 98871.80 | 92600.19 | 93010.01 | 50847.450960 | 4.883445e+09 | 8289691 | 23486.927050 | 2.255517e+09 | 1.978734e+07 | 4.58 | 5987.370117 | 1.047987 | 1.259382 | 0.006487 |
| 2658 | 2024-11-26 | 93010.01 | 94973.37 | 90791.10 | 91965.16 | 57858.731380 | 5.370919e+09 | 10225809 | 29120.267650 | 2.702544e+09 | 1.978734e+07 | 4.58 | 6021.629883 | 1.044430 | 1.253306 | 0.006478 |
2659 rows × 16 columns
La limpieza de datos consiste básicamente en la eliminación de los valores nulos, bien mediante la eliminación de las observaciones que los contienen, bien mediante la imputación de un valor representativo. En este caso, se ha aprovechado la fase de estudio de valores nulos para ejecutar la solución sobre la marcha, por lo que esta fase ya está realizada.
Se comprueba cómo el dataset ya contiene 2659 valores para cada variable, de un total de 2659 observaciones.
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 2659 entries, 0 to 2658 Data columns (total 16 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date 2659 non-null datetime64[ns] 1 open 2659 non-null float64 2 high 2659 non-null float64 3 low 2659 non-null float64 4 close 2659 non-null float64 5 VOL_BTC 2659 non-null float64 6 VOL_USDT 2659 non-null float64 7 transactions 2659 non-null int64 8 buy_market_BTC 2659 non-null float64 9 buy_market_USDT 2659 non-null float64 10 total_supply 2659 non-null float64 11 fed_funds_rate 2659 non-null float64 12 sp500 2659 non-null float64 13 eur_usd 2659 non-null float64 14 gbp_usd 2659 non-null float64 15 jpy_usd 2659 non-null float64 dtypes: datetime64[ns](1), float64(14), int64(1) memory usage: 332.5 KB
El análisis de variables ha puesto de manifiesto una relación lineal cuasi-perfecta entre las variables de precio de Bitcoin (open, high y low, obviando close por ser la variable target). Si bien, ante tal hecho, lo natural sería eliminar dos de las tres variables de precio, se ha establecido que dichas variables no son redundantes de forma directa, pues contienen información importante acerca del comportamiento del precio a lo largo de cada día (en qué precio abrió, qué precio máximo alcanzó, y en qué precio mínimo se frenó), por lo que su eliminación se descartó. Ahora bien, tampoco sería una buena práctica mantenerlas sin modificación alguna, por lo que se han decidido dos transformaciones simples que mantendrán la información aportada por las 3 variables de forma que se podrán eliminar y se evitará la posible multicolinealidad en algunos modelos.
Por un lado, se creará la variable range como diferencia entre high y low, y representará la amplitud de precios alcanzada cada día. Por otro lado, se creará la variable _delta_openclose como diferencia entre el precio de apertura y de cierre, representando la amplitud entre el primer y el último precio de cada día.
Además, antes de eliminar las variables de precio, se introducirá una nueva variable que representará el porcentaje de crecimiento o decrecimiento, según el caso, del precio de Bitcoin con respecto a su precio de apertura. Esto introduce la escala relativa del comporamiento del precio a lo largo del día, facilitando al modelo que pueda entender la magnitud del cambio del precio respecto de su nivel inicial. Al estar basada en las mismas variables que _delta_openclose, podría pensarse que se puede introducir una redundancia, aunque ésta no debería ser muy problemática ya que representa la información desde una perspectiva diferente. En cualquier caso, una vez introducidos los cambios, se estudiará su correlación para decidir si se mantiene o se elimina.
df['range'] = df['high'] - df['low']
df['delta_open_close'] = df['open'] - df['close']
df['percent_variation'] = (df['close'] - df['open']) / df['open']
df.drop(columns=['open', 'high', 'low'], inplace=True)
df
| date | close | VOL_BTC | VOL_USDT | transactions | buy_market_BTC | buy_market_USDT | total_supply | fed_funds_rate | sp500 | eur_usd | gbp_usd | jpy_usd | range | delta_open_close | percent_variation | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-08-17 | 4285.08 | 795.150377 | 3.454770e+06 | 3427 | 616.248541 | 2.678216e+06 | 1.651956e+07 | 1.16 | 2430.010010 | 1.177426 | 1.289158 | 0.009091 | 284.65 | -23.60 | 0.005538 |
| 1 | 2017-08-18 | 4108.37 | 1199.888264 | 5.086958e+06 | 5233 | 972.868710 | 4.129123e+06 | 1.651956e+07 | 1.16 | 2425.550049 | 1.171550 | 1.286505 | 0.009147 | 432.75 | 176.71 | -0.041238 |
| 2 | 2017-08-19 | 4139.98 | 381.309763 | 1.549484e+06 | 2153 | 274.336042 | 1.118002e+06 | 1.651956e+07 | 1.16 | 2425.550049 | 1.172909 | 1.286792 | 0.009147 | 334.69 | -31.61 | 0.007694 |
| 3 | 2017-08-20 | 4086.29 | 467.083022 | 1.930364e+06 | 2321 | 376.795947 | 1.557401e+06 | 1.651956e+07 | 1.16 | 2425.550049 | 1.174268 | 1.287079 | 0.009147 | 178.46 | 34.69 | -0.008418 |
| 4 | 2017-08-21 | 4016.00 | 691.743060 | 2.797232e+06 | 3972 | 557.356107 | 2.255663e+06 | 1.651956e+07 | 1.16 | 2428.370117 | 1.175627 | 1.287366 | 0.009147 | 207.83 | 53.13 | -0.013057 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2654 | 2024-11-22 | 98892.00 | 46189.309243 | 4.555537e+09 | 7271311 | 22709.668193 | 2.240635e+09 | 1.978572e+07 | 4.58 | 5969.339844 | 1.046934 | 1.258479 | 0.006484 | 2465.90 | -574.88 | 0.005847 |
| 2655 | 2024-11-23 | 97672.40 | 24757.843670 | 2.431610e+09 | 3839138 | 11176.248700 | 1.097539e+09 | 1.978626e+07 | 4.58 | 5969.339844 | 1.047285 | 1.258780 | 0.006485 | 1772.85 | 1219.60 | -0.012333 |
| 2656 | 2024-11-24 | 97900.04 | 31200.978380 | 3.034172e+09 | 4964720 | 15124.121760 | 1.471314e+09 | 1.978680e+07 | 4.58 | 5969.339844 | 1.047636 | 1.259081 | 0.006486 | 2829.23 | -227.64 | 0.002331 |
| 2657 | 2024-11-25 | 93010.01 | 50847.450960 | 4.883445e+09 | 8289691 | 23486.927050 | 2.255517e+09 | 1.978734e+07 | 4.58 | 5987.370117 | 1.047987 | 1.259382 | 0.006487 | 6271.61 | 4890.04 | -0.049949 |
| 2658 | 2024-11-26 | 91965.16 | 57858.731380 | 5.370919e+09 | 10225809 | 29120.267650 | 2.702544e+09 | 1.978734e+07 | 4.58 | 6021.629883 | 1.044430 | 1.253306 | 0.006478 | 4182.27 | 1044.85 | -0.011234 |
2659 rows × 16 columns
df[['close', 'sp500', 'range', 'delta_open_close', 'percent_variation']].corr()
| close | sp500 | range | delta_open_close | percent_variation | |
|---|---|---|---|---|---|
| close | 1.000000 | 0.916830 | 0.670597 | -0.058786 | 0.019800 |
| sp500 | 0.916830 | 1.000000 | 0.525607 | -0.033450 | -0.001138 |
| range | 0.670597 | 0.525607 | 1.000000 | 0.031196 | -0.035328 |
| delta_open_close | -0.058786 | -0.033450 | 0.031196 | 1.000000 | -0.761566 |
| percent_variation | 0.019800 | -0.001138 | -0.035328 | -0.761566 | 1.000000 |
Al realizar el estudio de correlaciones entre las nuevas variables y la variable target se decide estudiar también su relación con la variable sp500, ya que las variables en las que se basan las nuevas estaban altamente correlacionadas con sp500 y es interesante ver que esta correlación ha desaparecido. Por otro lado, la correlación entre _delta_openclose y _percentvariation es moderadamente elevada y negativa (-0.76), por lo que no existirán problemas graves de multicolinealidad entre ellas.
La baja correlación entre _delta_openclose y _percentvariation con close no significa necesariamente que no sean variables válidas, pues podrían aportar información complementaria al modelo.
Por otro lado, también durante el estudio de correlaciones se detectó una muy fuerte relación lineal entre las variables de volumen y las variables de compras a mercado, por lo que también por aquí podrían surgir problemas de multicolinealidad. Para evitarla y no perder información relevante sobre las compras a mercado o el volumen total, en lugar de eliminar una de ellas, se transformará la variable de compras de mercado como porcentaje sobre el volumen total. De igual modo, se estudiará posteriormente las correlaciones entre las variables de volumen y los porcentajes obtenidos para comprobar que se ha reducido el problema de la multicolinealidad.
df['percent_buy_market_BTC'] = df['buy_market_BTC'] / df['VOL_BTC']
df['percent_buy_market_USDT'] = df['buy_market_USDT'] / df['VOL_USDT']
df.drop(columns=['buy_market_BTC', 'buy_market_USDT'], inplace=True)
df
| date | close | VOL_BTC | VOL_USDT | transactions | total_supply | fed_funds_rate | sp500 | eur_usd | gbp_usd | jpy_usd | range | delta_open_close | percent_variation | percent_buy_market_BTC | percent_buy_market_USDT | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-08-17 | 4285.08 | 795.150377 | 3.454770e+06 | 3427 | 1.651956e+07 | 1.16 | 2430.010010 | 1.177426 | 1.289158 | 0.009091 | 284.65 | -23.60 | 0.005538 | 0.775009 | 0.775223 |
| 1 | 2017-08-18 | 4108.37 | 1199.888264 | 5.086958e+06 | 5233 | 1.651956e+07 | 1.16 | 2425.550049 | 1.171550 | 1.286505 | 0.009147 | 432.75 | 176.71 | -0.041238 | 0.810799 | 0.811708 |
| 2 | 2017-08-19 | 4139.98 | 381.309763 | 1.549484e+06 | 2153 | 1.651956e+07 | 1.16 | 2425.550049 | 1.172909 | 1.286792 | 0.009147 | 334.69 | -31.61 | 0.007694 | 0.719457 | 0.721532 |
| 3 | 2017-08-20 | 4086.29 | 467.083022 | 1.930364e+06 | 2321 | 1.651956e+07 | 1.16 | 2425.550049 | 1.174268 | 1.287079 | 0.009147 | 178.46 | 34.69 | -0.008418 | 0.806700 | 0.806791 |
| 4 | 2017-08-21 | 4016.00 | 691.743060 | 2.797232e+06 | 3972 | 1.651956e+07 | 1.16 | 2428.370117 | 1.175627 | 1.287366 | 0.009147 | 207.83 | 53.13 | -0.013057 | 0.805727 | 0.806391 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2654 | 2024-11-22 | 98892.00 | 46189.309243 | 4.555537e+09 | 7271311 | 1.978572e+07 | 4.58 | 5969.339844 | 1.046934 | 1.258479 | 0.006484 | 2465.90 | -574.88 | 0.005847 | 0.491665 | 0.491849 |
| 2655 | 2024-11-23 | 97672.40 | 24757.843670 | 2.431610e+09 | 3839138 | 1.978626e+07 | 4.58 | 5969.339844 | 1.047285 | 1.258780 | 0.006485 | 1772.85 | 1219.60 | -0.012333 | 0.451423 | 0.451363 |
| 2656 | 2024-11-24 | 97900.04 | 31200.978380 | 3.034172e+09 | 4964720 | 1.978680e+07 | 4.58 | 5969.339844 | 1.047636 | 1.259081 | 0.006486 | 2829.23 | -227.64 | 0.002331 | 0.484732 | 0.484914 |
| 2657 | 2024-11-25 | 93010.01 | 50847.450960 | 4.883445e+09 | 8289691 | 1.978734e+07 | 4.58 | 5987.370117 | 1.047987 | 1.259382 | 0.006487 | 6271.61 | 4890.04 | -0.049949 | 0.461910 | 0.461870 |
| 2658 | 2024-11-26 | 91965.16 | 57858.731380 | 5.370919e+09 | 10225809 | 1.978734e+07 | 4.58 | 6021.629883 | 1.044430 | 1.253306 | 0.006478 | 4182.27 | 1044.85 | -0.011234 | 0.503299 | 0.503181 |
2659 rows × 16 columns
df[['close', 'VOL_BTC', 'VOL_USDT', 'percent_buy_market_BTC', 'percent_buy_market_USDT']].corr()
| close | VOL_BTC | VOL_USDT | percent_buy_market_BTC | percent_buy_market_USDT | |
|---|---|---|---|---|---|
| close | 1.000000 | -0.051808 | 0.402791 | -0.108420 | -0.109291 |
| VOL_BTC | -0.051808 | 1.000000 | 0.838231 | -0.006600 | -0.007067 |
| VOL_USDT | 0.402791 | 0.838231 | 1.000000 | -0.052582 | -0.053162 |
| percent_buy_market_BTC | -0.108420 | -0.006600 | -0.052582 | 1.000000 | 0.999965 |
| percent_buy_market_USDT | -0.109291 | -0.007067 | -0.053162 | 0.999965 | 1.000000 |
Al estudiar las correlaciones entre las variables de volumen y las nuevas que representan el porcentaje de compras a mercado con respecto al volumen total, se observa que se ha diluido el problema de multicolinealidad entre ellas, ya que se pasa de unas correlaciones cuasi-perfectas a unas correlaciones negativas muy cercanas a 0. Pero ha surgido un problema de correlación perfecta entre las dos nuevas variables, algo que tiene sentido ya que, si bien la cantidad de Bitcoin y USD negociadas en un día no son iguales, los porcentajes que para ambas representan las compras a mercado con respecto de sus volúmenes sí son idénticos. De ahí que la correlación entre ambas nuevas variables sea prácticamente perfecta, por lo que se procederá a eliminar una de ellas y renombrar la otra como _percent_buymarket.
De esta manera, se mantiene la información aportada por las variables de _buymarket sobre el comportamiento relativo del volumen de compras a mercado y se elimina el problema de la multicolinealidad que existía debido a su alta correlación con las variables de volumen.
df.drop(columns=['percent_buy_market_USDT'], inplace=True)
df.rename(columns={'percent_buy_market_BTC': 'percent_buy_market'}, inplace=True)
df
| date | close | VOL_BTC | VOL_USDT | transactions | total_supply | fed_funds_rate | sp500 | eur_usd | gbp_usd | jpy_usd | range | delta_open_close | percent_variation | percent_buy_market | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-08-17 | 4285.08 | 795.150377 | 3.454770e+06 | 3427 | 1.651956e+07 | 1.16 | 2430.010010 | 1.177426 | 1.289158 | 0.009091 | 284.65 | -23.60 | 0.005538 | 0.775009 |
| 1 | 2017-08-18 | 4108.37 | 1199.888264 | 5.086958e+06 | 5233 | 1.651956e+07 | 1.16 | 2425.550049 | 1.171550 | 1.286505 | 0.009147 | 432.75 | 176.71 | -0.041238 | 0.810799 |
| 2 | 2017-08-19 | 4139.98 | 381.309763 | 1.549484e+06 | 2153 | 1.651956e+07 | 1.16 | 2425.550049 | 1.172909 | 1.286792 | 0.009147 | 334.69 | -31.61 | 0.007694 | 0.719457 |
| 3 | 2017-08-20 | 4086.29 | 467.083022 | 1.930364e+06 | 2321 | 1.651956e+07 | 1.16 | 2425.550049 | 1.174268 | 1.287079 | 0.009147 | 178.46 | 34.69 | -0.008418 | 0.806700 |
| 4 | 2017-08-21 | 4016.00 | 691.743060 | 2.797232e+06 | 3972 | 1.651956e+07 | 1.16 | 2428.370117 | 1.175627 | 1.287366 | 0.009147 | 207.83 | 53.13 | -0.013057 | 0.805727 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2654 | 2024-11-22 | 98892.00 | 46189.309243 | 4.555537e+09 | 7271311 | 1.978572e+07 | 4.58 | 5969.339844 | 1.046934 | 1.258479 | 0.006484 | 2465.90 | -574.88 | 0.005847 | 0.491665 |
| 2655 | 2024-11-23 | 97672.40 | 24757.843670 | 2.431610e+09 | 3839138 | 1.978626e+07 | 4.58 | 5969.339844 | 1.047285 | 1.258780 | 0.006485 | 1772.85 | 1219.60 | -0.012333 | 0.451423 |
| 2656 | 2024-11-24 | 97900.04 | 31200.978380 | 3.034172e+09 | 4964720 | 1.978680e+07 | 4.58 | 5969.339844 | 1.047636 | 1.259081 | 0.006486 | 2829.23 | -227.64 | 0.002331 | 0.484732 |
| 2657 | 2024-11-25 | 93010.01 | 50847.450960 | 4.883445e+09 | 8289691 | 1.978734e+07 | 4.58 | 5987.370117 | 1.047987 | 1.259382 | 0.006487 | 6271.61 | 4890.04 | -0.049949 | 0.461910 |
| 2658 | 2024-11-26 | 91965.16 | 57858.731380 | 5.370919e+09 | 10225809 | 1.978734e+07 | 4.58 | 6021.629883 | 1.044430 | 1.253306 | 0.006478 | 4182.27 | 1044.85 | -0.011234 | 0.503299 |
2659 rows × 15 columns
Al tratarse del precio de un activo financiero, además de todas las variables vistas hasta el momento, existen una serie de indicadores técnicos muy empleados por los operadores de mercado para obtener información adicional sobre el precio, por lo que se procederá a crear dichos indicadores como variables y a comprobar si pueden ayudar al modelo a comprender mejor el comportamiento y a predecir de una manera más acertada el precio de Bitcoin.
Los indicadores que se calcularán serán: media móvil simple (sma), índice de fuerza relativa (rsi), bandas de bollinger (_bollingerupper y _bollingerlower), MACD (macd) y volatilidad histórica (_historicalvolatility).
df['sma_7'] = sma(df)
df['sma_20'] = sma(df, 20)
df['rsi'] = rsi(df)
df['bollinger_upper'], df['bollinger_lower'] = bollinger_bands(df)
df['macd'], df['signal_line'] = macd(df)
df['historical_volatility'] = historical_volatility(df)
df
| date | close | VOL_BTC | VOL_USDT | transactions | total_supply | fed_funds_rate | sp500 | eur_usd | gbp_usd | ... | percent_variation | percent_buy_market | sma_7 | sma_20 | rsi | bollinger_upper | bollinger_lower | macd | signal_line | historical_volatility | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-08-17 | 4285.08 | 795.150377 | 3.454770e+06 | 3427 | 1.651956e+07 | 1.16 | 2430.010010 | 1.177426 | 1.289158 | ... | 0.005538 | 0.775009 | NaN | NaN | NaN | NaN | NaN | 0.000000 | 0.000000 | NaN |
| 1 | 2017-08-18 | 4108.37 | 1199.888264 | 5.086958e+06 | 5233 | 1.651956e+07 | 1.16 | 2425.550049 | 1.171550 | 1.286505 | ... | -0.041238 | 0.810799 | NaN | NaN | NaN | NaN | NaN | -14.096524 | -2.819305 | NaN |
| 2 | 2017-08-19 | 4139.98 | 381.309763 | 1.549484e+06 | 2153 | 1.651956e+07 | 1.16 | 2425.550049 | 1.172909 | 1.286792 | ... | 0.007694 | 0.719457 | NaN | NaN | NaN | NaN | NaN | -22.458570 | -6.747158 | NaN |
| 3 | 2017-08-20 | 4086.29 | 467.083022 | 1.930364e+06 | 2321 | 1.651956e+07 | 1.16 | 2425.550049 | 1.174268 | 1.287079 | ... | -0.008418 | 0.806700 | NaN | NaN | NaN | NaN | NaN | -33.037055 | -12.005137 | NaN |
| 4 | 2017-08-21 | 4016.00 | 691.743060 | 2.797232e+06 | 3972 | 1.651956e+07 | 1.16 | 2428.370117 | 1.175627 | 1.287366 | ... | -0.013057 | 0.805727 | NaN | NaN | NaN | NaN | NaN | -46.555731 | -18.915256 | NaN |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2654 | 2024-11-22 | 98892.00 | 46189.309243 | 4.555537e+09 | 7271311 | 1.978572e+07 | 4.58 | 5969.339844 | 1.046934 | 1.258479 | ... | 0.005847 | 0.491665 | 93530.494286 | 84551.5730 | 84.727434 | 104126.985845 | 64976.160155 | 6994.475759 | 6040.286951 | 0.032602 |
| 2655 | 2024-11-23 | 97672.40 | 24757.843670 | 2.431610e+09 | 3839138 | 1.978626e+07 | 4.58 | 5969.339844 | 1.047285 | 1.258780 | ... | -0.012333 | 0.451423 | 94542.705714 | 85996.3935 | 81.545267 | 104924.076928 | 67068.710072 | 7019.055192 | 6236.040599 | 0.032772 |
| 2656 | 2024-11-24 | 97900.04 | 31200.978380 | 3.034172e+09 | 4964720 | 1.978680e+07 | 4.58 | 5969.339844 | 1.047636 | 1.259081 | ... | 0.002331 | 0.484732 | 95691.855714 | 87498.8950 | 79.400401 | 105084.637135 | 69913.152865 | 6976.482666 | 6384.129013 | 0.032165 |
| 2657 | 2024-11-25 | 93010.01 | 50847.450960 | 4.883445e+09 | 8289691 | 1.978734e+07 | 4.58 | 5987.370117 | 1.047987 | 1.259382 | ... | -0.049949 | 0.461910 | 96055.560000 | 88680.7950 | 58.253704 | 104191.930064 | 73169.659936 | 6473.536184 | 6402.010447 | 0.035699 |
| 2658 | 2024-11-26 | 91965.16 | 57858.731380 | 5.370919e+09 | 10225809 | 1.978734e+07 | 4.58 | 6021.629883 | 1.044430 | 1.253306 | ... | -0.011234 | 0.503299 | 96006.184286 | 89500.4535 | 57.494634 | 103778.424835 | 75222.482165 | 5922.367445 | 6306.081847 | 0.031942 |
2659 rows × 23 columns
df[['close', 'sma_7', 'sma_20', 'rsi', 'bollinger_upper', 'bollinger_lower', 'macd',
'signal_line', 'historical_volatility']].corr()
| close | sma_7 | sma_20 | rsi | bollinger_upper | bollinger_lower | macd | signal_line | historical_volatility | |
|---|---|---|---|---|---|---|---|---|---|
| close | 1.000000 | 0.997428 | 0.990833 | 0.090884 | 0.990029 | 0.980983 | 0.307554 | 0.310510 | -0.162180 |
| sma_7 | 0.997428 | 1.000000 | 0.995764 | 0.056722 | 0.993946 | 0.987106 | 0.284278 | 0.299288 | -0.160362 |
| sma_20 | 0.990833 | 0.995764 | 1.000000 | -0.004526 | 0.996017 | 0.993961 | 0.207282 | 0.236186 | -0.153283 |
| rsi | 0.090884 | 0.056722 | -0.004526 | 1.000000 | 0.009890 | -0.022219 | 0.578921 | 0.443256 | -0.113285 |
| bollinger_upper | 0.990029 | 0.993946 | 0.996017 | 0.009890 | 1.000000 | 0.980218 | 0.227827 | 0.252635 | -0.112249 |
| bollinger_lower | 0.980983 | 0.987106 | 0.993961 | -0.022219 | 0.980218 | 1.000000 | 0.179731 | 0.213359 | -0.202105 |
| macd | 0.307554 | 0.284278 | 0.207282 | 0.578921 | 0.227827 | 0.179731 | 1.000000 | 0.961758 | -0.106240 |
| signal_line | 0.310510 | 0.299288 | 0.236186 | 0.443256 | 0.252635 | 0.213359 | 0.961758 | 1.000000 | -0.095938 |
| historical_volatility | -0.162180 | -0.160362 | -0.153283 | -0.113285 | -0.112249 | -0.202105 | -0.106240 | -0.095938 | 1.000000 |
df[['close', 'sma_7', 'sma_20', 'rsi', 'bollinger_upper', 'bollinger_lower', 'macd',
'signal_line', 'historical_volatility']].corr('spearman')
| close | sma_7 | sma_20 | rsi | bollinger_upper | bollinger_lower | macd | signal_line | historical_volatility | |
|---|---|---|---|---|---|---|---|---|---|
| close | 1.000000 | 0.997504 | 0.991365 | 0.079953 | 0.988848 | 0.982785 | 0.206665 | 0.208557 | -0.131333 |
| sma_7 | 0.997504 | 1.000000 | 0.995806 | 0.043713 | 0.992648 | 0.987839 | 0.189582 | 0.201167 | -0.130933 |
| sma_20 | 0.991365 | 0.995806 | 1.000000 | -0.024095 | 0.996039 | 0.992399 | 0.130636 | 0.154060 | -0.126223 |
| rsi | 0.079953 | 0.043713 | -0.024095 | 1.000000 | -0.016045 | -0.038707 | 0.680448 | 0.519558 | -0.077607 |
| bollinger_upper | 0.988848 | 0.992648 | 0.996039 | -0.016045 | 1.000000 | 0.979700 | 0.133550 | 0.154909 | -0.076015 |
| bollinger_lower | 0.982785 | 0.987839 | 0.992399 | -0.038707 | 0.979700 | 1.000000 | 0.117664 | 0.143454 | -0.192104 |
| macd | 0.206665 | 0.189582 | 0.130636 | 0.680448 | 0.133550 | 0.117664 | 1.000000 | 0.948397 | -0.088501 |
| signal_line | 0.208557 | 0.201167 | 0.154060 | 0.519558 | 0.154909 | 0.143454 | 0.948397 | 1.000000 | -0.094106 |
| historical_volatility | -0.131333 | -0.130933 | -0.126223 | -0.077607 | -0.076015 | -0.192104 | -0.088501 | -0.094106 | 1.000000 |
Del análisis de correlaciones entre las nuevas variables se destaca lo siguiente:
df['bollinger_mean'] = (df['bollinger_upper'] + df['bollinger_lower']) / 2
df.drop(columns=['sma_20', 'rsi', 'bollinger_upper', 'bollinger_lower', 'signal_line'], inplace=True)
df
| date | close | VOL_BTC | VOL_USDT | transactions | total_supply | fed_funds_rate | sp500 | eur_usd | gbp_usd | jpy_usd | range | delta_open_close | percent_variation | percent_buy_market | sma_7 | macd | historical_volatility | bollinger_mean | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-08-17 | 4285.08 | 795.150377 | 3.454770e+06 | 3427 | 1.651956e+07 | 1.16 | 2430.010010 | 1.177426 | 1.289158 | 0.009091 | 284.65 | -23.60 | 0.005538 | 0.775009 | NaN | 0.000000 | NaN | NaN |
| 1 | 2017-08-18 | 4108.37 | 1199.888264 | 5.086958e+06 | 5233 | 1.651956e+07 | 1.16 | 2425.550049 | 1.171550 | 1.286505 | 0.009147 | 432.75 | 176.71 | -0.041238 | 0.810799 | NaN | -14.096524 | NaN | NaN |
| 2 | 2017-08-19 | 4139.98 | 381.309763 | 1.549484e+06 | 2153 | 1.651956e+07 | 1.16 | 2425.550049 | 1.172909 | 1.286792 | 0.009147 | 334.69 | -31.61 | 0.007694 | 0.719457 | NaN | -22.458570 | NaN | NaN |
| 3 | 2017-08-20 | 4086.29 | 467.083022 | 1.930364e+06 | 2321 | 1.651956e+07 | 1.16 | 2425.550049 | 1.174268 | 1.287079 | 0.009147 | 178.46 | 34.69 | -0.008418 | 0.806700 | NaN | -33.037055 | NaN | NaN |
| 4 | 2017-08-21 | 4016.00 | 691.743060 | 2.797232e+06 | 3972 | 1.651956e+07 | 1.16 | 2428.370117 | 1.175627 | 1.287366 | 0.009147 | 207.83 | 53.13 | -0.013057 | 0.805727 | NaN | -46.555731 | NaN | NaN |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2654 | 2024-11-22 | 98892.00 | 46189.309243 | 4.555537e+09 | 7271311 | 1.978572e+07 | 4.58 | 5969.339844 | 1.046934 | 1.258479 | 0.006484 | 2465.90 | -574.88 | 0.005847 | 0.491665 | 93530.494286 | 6994.475759 | 0.032602 | 84551.5730 |
| 2655 | 2024-11-23 | 97672.40 | 24757.843670 | 2.431610e+09 | 3839138 | 1.978626e+07 | 4.58 | 5969.339844 | 1.047285 | 1.258780 | 0.006485 | 1772.85 | 1219.60 | -0.012333 | 0.451423 | 94542.705714 | 7019.055192 | 0.032772 | 85996.3935 |
| 2656 | 2024-11-24 | 97900.04 | 31200.978380 | 3.034172e+09 | 4964720 | 1.978680e+07 | 4.58 | 5969.339844 | 1.047636 | 1.259081 | 0.006486 | 2829.23 | -227.64 | 0.002331 | 0.484732 | 95691.855714 | 6976.482666 | 0.032165 | 87498.8950 |
| 2657 | 2024-11-25 | 93010.01 | 50847.450960 | 4.883445e+09 | 8289691 | 1.978734e+07 | 4.58 | 5987.370117 | 1.047987 | 1.259382 | 0.006487 | 6271.61 | 4890.04 | -0.049949 | 0.461910 | 96055.560000 | 6473.536184 | 0.035699 | 88680.7950 |
| 2658 | 2024-11-26 | 91965.16 | 57858.731380 | 5.370919e+09 | 10225809 | 1.978734e+07 | 4.58 | 6021.629883 | 1.044430 | 1.253306 | 0.006478 | 4182.27 | 1044.85 | -0.011234 | 0.503299 | 96006.184286 | 5922.367445 | 0.031942 | 89500.4535 |
2659 rows × 19 columns
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 2659 entries, 0 to 2658 Data columns (total 19 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date 2659 non-null datetime64[ns] 1 close 2659 non-null float64 2 VOL_BTC 2659 non-null float64 3 VOL_USDT 2659 non-null float64 4 transactions 2659 non-null int64 5 total_supply 2659 non-null float64 6 fed_funds_rate 2659 non-null float64 7 sp500 2659 non-null float64 8 eur_usd 2659 non-null float64 9 gbp_usd 2659 non-null float64 10 jpy_usd 2659 non-null float64 11 range 2659 non-null float64 12 delta_open_close 2659 non-null float64 13 percent_variation 2659 non-null float64 14 percent_buy_market 2659 non-null float64 15 sma_7 2653 non-null float64 16 macd 2659 non-null float64 17 historical_volatility 2639 non-null float64 18 bollinger_mean 2640 non-null float64 dtypes: datetime64[ns](1), float64(17), int64(1) memory usage: 394.8 KB
Una vez creada las nuevas variables basadas en indicadores técnicos financieros, se advierte que existen valores nulos para algunas de ellas, al estar basadas en medias de varios días atrás. Por ejemplo, para la SMA de 7 días se necesitan los datos de los 6 días anteriores más el día en cuestión, por lo que los 6 primeros días del dataset no tendrán dato para _sma7 al no contar con información suficiente para calcular la media. Al tratarse de las 20 primeras observaciones, y dado que la imputación mediante interpolación forward fill (que sería la que habría que usar en este caso) puede introducir suposiciones que podrían no reflejar el comportamiento real de las variables, se opta en esta ocasión por eliminar dichas observaciones.
df.dropna(inplace=True)
df.reset_index(drop=True, inplace=True)
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 2639 entries, 0 to 2638 Data columns (total 19 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 date 2639 non-null datetime64[ns] 1 close 2639 non-null float64 2 VOL_BTC 2639 non-null float64 3 VOL_USDT 2639 non-null float64 4 transactions 2639 non-null int64 5 total_supply 2639 non-null float64 6 fed_funds_rate 2639 non-null float64 7 sp500 2639 non-null float64 8 eur_usd 2639 non-null float64 9 gbp_usd 2639 non-null float64 10 jpy_usd 2639 non-null float64 11 range 2639 non-null float64 12 delta_open_close 2639 non-null float64 13 percent_variation 2639 non-null float64 14 percent_buy_market 2639 non-null float64 15 sma_7 2639 non-null float64 16 macd 2639 non-null float64 17 historical_volatility 2639 non-null float64 18 bollinger_mean 2639 non-null float64 dtypes: datetime64[ns](1), float64(17), int64(1) memory usage: 391.9 KB
Para finalizar la preparación de los datos, se procederá a la normalización de los mismos. Este paso es importante para ayudar a aquellos modelos sensibles a la escala (como las redes neuronales recurrentes) a que estimen bien el precio de Bitcoin. Se seguirán dos estrategias distintas de normalización, seleccionando una serie de variables que serán normalizadas en ventanas de tiempo y otras que lo serán de manera global. La normalización en ventanas de tiempo es útil en series temporales para reflejar los cambios dinámicos en el contexto de las variables, previniendo fugas de información del futuro al normalizar con datos muy anteriores y capturando patrones locales en lugar de a una escala global que podría no ser representativa en el largo plazo.
norm_20 = ['close', 'VOL_BTC', 'VOL_USDT', 'transactions', 'percent_buy_market', 'sp500', 'eur_usd', 'gbp_usd', 'jpy_usd',
'sma_7', 'macd', 'historical_volatility', 'bollinger_mean']
norm_7 = ['range', 'delta_open_close', 'percent_variation']
norm_global = ['total_supply', 'fed_funds_rate']
df_normalized = df.copy()
for var in norm_global:
df_normalized[var] = normalize_zscore(df, var)
for var in norm_7:
df_normalized[var] = normalize_zscore(df, var, _window=7)
for var in norm_20:
df_normalized[var] = normalize_zscore(df, var, _window=20)
df_normalized
| date | close | VOL_BTC | VOL_USDT | transactions | total_supply | fed_funds_rate | sp500 | eur_usd | gbp_usd | jpy_usd | range | delta_open_close | percent_variation | percent_buy_market | sma_7 | macd | historical_volatility | bollinger_mean | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-09-06 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | -2.123873 | -0.559575 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
| 1 | 2017-09-07 | 1.000000 | -1.000000 | -1.000000 | -1.000000 | -2.121590 | -0.559575 | -1.000000 | 1.000000 | 1.000000 | -1.000000 | 1.000000 | 1.000000 | -1.000000 | -1.000000 | -1.000000 | 1.000000 | -1.000000 | 1.000000 |
| 2 | 2017-09-08 | -1.394931 | 1.236182 | 1.211733 | -0.256371 | -2.119306 | -0.559575 | -1.407479 | 1.413532 | 1.403426 | 1.122117 | 1.412178 | 1.363598 | -1.355800 | -1.309103 | -1.412401 | -1.023541 | 1.373680 | 0.922188 |
| 3 | 2017-09-09 | -1.052193 | -0.497055 | -0.608152 | -0.592224 | -2.117023 | -0.559575 | -0.996818 | 0.961739 | 1.352940 | 0.868416 | -0.814725 | -0.012162 | -0.015227 | -0.612001 | -1.283470 | -1.464938 | 0.960669 | 1.026464 |
| 4 | 2017-09-10 | -1.216382 | -0.428307 | -0.618447 | -0.655099 | -2.114739 | -0.559575 | -0.814330 | 0.737161 | 1.384527 | 0.750490 | 0.345394 | 0.368817 | -0.420229 | -0.438382 | -1.456822 | -1.644020 | 0.895415 | 1.042165 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2634 | 2024-11-22 | 1.503209 | -0.189959 | 0.107295 | 0.137776 | 1.381010 | 1.195509 | 0.643943 | -1.478081 | -1.484743 | -0.194169 | -0.718971 | 0.360430 | -0.379454 | -0.735712 | 1.485522 | 1.338367 | 0.543889 | 1.933734 |
| 2635 | 2024-11-23 | 1.265800 | -1.054117 | -0.908510 | -1.245873 | 1.381595 | 1.195509 | 0.575544 | -1.321903 | -1.311802 | -0.106426 | -1.485190 | 1.350555 | -1.327074 | -2.729985 | 1.436747 | 1.226510 | 0.497017 | 1.908522 |
| 2636 | 2024-11-24 | 1.213637 | -0.798832 | -0.661910 | -0.780186 | 1.382181 | 1.195509 | 0.532303 | -1.190516 | -1.167351 | -0.014875 | -0.359405 | 0.599217 | -0.622036 | -0.801017 | 1.414607 | 1.107554 | 0.288867 | 1.889185 |
| 2637 | 2024-11-25 | 0.572709 | -0.040178 | 0.170503 | 0.487972 | 1.382766 | 1.195509 | 0.854844 | -1.080196 | -1.041109 | 0.087710 | 1.875818 | 1.997675 | -1.981597 | -1.777760 | 1.329796 | 0.773773 | 1.167842 | 1.827903 |
| 2638 | 2024-11-26 | 0.354215 | 0.391267 | 0.499192 | 1.196128 | 1.382766 | 1.195509 | 1.447457 | -1.366222 | -1.356228 | 0.019515 | 0.344444 | 0.384151 | -0.409982 | 0.357434 | 1.220564 | 0.381991 | -0.042660 | 1.731872 |
2639 rows × 19 columns
Debido a que la normalización en ventanas de tiempo requiere conocer datos hacia atrás para el cálculo de la media y la desviación (es decir, requieren de una ventana completa de x días hacia atrás), las primeras observaciones tienen valores 0, 1 y -1 para muchas de sus variables normalizadas, lo que puede llevar a problemas de consistencia en los datos e introducir un pequeño sesgo en los modelos a entrenar, pues sus estadísticas no son comparables al resto. Por este motivo, se decide eliminar estas observaciones.
Dado que la ventana máxima que se ha empleado en todas las normalizaciones es de 20 días, las primeras 19 observaciones no cuentan con ventanas completas para realizar la normalización de las variables que se han normalizado con ventanas de 20 días, por lo que sus datos no son consistentes y serán las observaciones eliminadas.
df_ready = df_normalized.iloc[19:].reset_index(drop=True).copy()
df_ready
| date | close | VOL_BTC | VOL_USDT | transactions | total_supply | fed_funds_rate | sp500 | eur_usd | gbp_usd | jpy_usd | range | delta_open_close | percent_variation | percent_buy_market | sma_7 | macd | historical_volatility | bollinger_mean | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017-09-25 | -0.085401 | -0.560892 | -0.699981 | 1.261409 | -2.082601 | -0.559575 | 0.350046 | -0.592119 | 0.824893 | -1.274712 | 1.144172 | -1.577317 | 1.589879 | -0.755249 | -0.949703 | -0.641271 | 0.795666 | -1.846998 |
| 1 | 2017-09-26 | -0.099713 | -1.063747 | -1.215896 | 0.447806 | -2.080645 | -0.559575 | 0.284031 | -1.957006 | 0.592814 | -0.768540 | -1.602020 | 0.266043 | -0.277257 | -0.444980 | -0.878831 | -0.397677 | 0.623934 | -1.794520 |
| 2 | 2017-09-27 | 1.122924 | -0.814737 | -0.818137 | 0.556466 | -2.078689 | -0.559575 | 0.860033 | -2.620322 | 0.344516 | -1.046032 | 1.299942 | -1.331452 | 1.283955 | 1.242939 | -0.654669 | 0.168071 | 0.705831 | -1.695572 |
| 3 | 2017-09-28 | 1.102849 | -0.152215 | 0.091193 | 1.820999 | -2.076600 | -0.559575 | 1.025069 | -2.568349 | -0.052404 | -1.236413 | -0.561697 | 0.645138 | -0.645081 | -0.483257 | -0.259479 | 0.715206 | 0.429499 | -1.530909 |
| 4 | 2017-09-29 | 1.142915 | 1.723521 | 2.239944 | 2.727527 | -2.074512 | -0.610893 | 1.800645 | -1.846679 | 0.064926 | -0.909286 | 0.671190 | 0.550353 | -0.559231 | -0.690897 | 0.201619 | 1.241301 | 0.348591 | -1.394947 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2615 | 2024-11-22 | 1.503209 | -0.189959 | 0.107295 | 0.137776 | 1.381010 | 1.195509 | 0.643943 | -1.478081 | -1.484743 | -0.194169 | -0.718971 | 0.360430 | -0.379454 | -0.735712 | 1.485522 | 1.338367 | 0.543889 | 1.933734 |
| 2616 | 2024-11-23 | 1.265800 | -1.054117 | -0.908510 | -1.245873 | 1.381595 | 1.195509 | 0.575544 | -1.321903 | -1.311802 | -0.106426 | -1.485190 | 1.350555 | -1.327074 | -2.729985 | 1.436747 | 1.226510 | 0.497017 | 1.908522 |
| 2617 | 2024-11-24 | 1.213637 | -0.798832 | -0.661910 | -0.780186 | 1.382181 | 1.195509 | 0.532303 | -1.190516 | -1.167351 | -0.014875 | -0.359405 | 0.599217 | -0.622036 | -0.801017 | 1.414607 | 1.107554 | 0.288867 | 1.889185 |
| 2618 | 2024-11-25 | 0.572709 | -0.040178 | 0.170503 | 0.487972 | 1.382766 | 1.195509 | 0.854844 | -1.080196 | -1.041109 | 0.087710 | 1.875818 | 1.997675 | -1.981597 | -1.777760 | 1.329796 | 0.773773 | 1.167842 | 1.827903 |
| 2619 | 2024-11-26 | 0.354215 | 0.391267 | 0.499192 | 1.196128 | 1.382766 | 1.195509 | 1.447457 | -1.366222 | -1.356228 | 0.019515 | 0.344444 | 0.384151 | -0.409982 | 0.357434 | 1.220564 | 0.381991 | -0.042660 | 1.731872 |
2620 rows × 19 columns
Debido a que 17 variables predictivas no representan una complejidad excesiva para una red neuronal, se decide no realizar una reducción de dimensionalidad, al menos por el momento. Una vez entrenado el modelo principal, en función de sus resultados se tomará la decisión de si reducir la dimensionalidad o no de cara al entrenamiento de modelos más simples con los que comparar resultados, para lo que se emplearán los valores de SHAP (Shapley Additive Explanations), que evalúan la importancia de las características en función del impacto que han tenido en las predicciones del modelo.
Por lo tanto, el dataset final, una vez realizadas todas las transformaciones, eliminaciones, adiciones y normalizaciones, consta de 2619 observaciones, 17 variables explicativas, 1 variable fecha y la variable objetivo.
# Se guardan en formato csv el dataframe que contiene todos los datos preparados para entrenar el modelo
save_csv(df_ready, "df", "ready")
'''Se guarda en formato csv un dataframe con los precios de cierre de Bitcoin; estos datos servirán posteriormente
para desnormalizar los valores predichos por el modelo, ya que, al haber sido normalizados en ventanas de tiempo,
no es posible emplear una media y una desviación únicas para todos los datos.
También se empleará para comparar los valores predichos con los valores reales.'''
save_csv(df.close[19:], "df", "close")